fix: photo URLs use ADMIN_URL prefix for Telegram API + resilience improvements
- productHandler, purchaseHandler, viewHandler: prefix relative photo_url with ADMIN_URL so Telegram can fetch images via public URL - bot.js: 5 retries with 5s delay on init, graceful fallback to null - errorHandler.js: 5 retries on 404 (invalid token), stops polling but keeps process alive for admin panel - config.js: BOT_TOKEN missing logs warning instead of process.exit - index.js: bot handlers only registered when bot is available, admin panel always starts regardless of bot status - adminWalletsHandler.js: replace throw with logger.warn for missing commission wallets (prevents container crash on startup) - docker-compose.yml: bind admin port to all interfaces (0.0.0.0) - README.md: updated with Tor proxy architecture, resilience docs - install.sh: added Tor proxy status check and onion address display
This commit is contained in:
176
README.md
176
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Telegram Shop Bot
|
# Telegram Shop Bot
|
||||||
|
|
||||||
Телеграм-бот для организации онлайн-продаж через Telegram с поддержкой криптовалют и WireGuard VPN.
|
Телеграм-бот для организации онлайн-продаж через Telegram с поддержкой криптовалют, WireGuard VPN и Tor-прокси для доступа к админ-панели через onion-адрес.
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
- История транзакций и покупок
|
- История транзакций и покупок
|
||||||
- SaaS-система с автоматическим расчётом комиссий
|
- SaaS-система с автоматическим расчётом комиссий
|
||||||
- Админ-панель на порту 3001
|
- Админ-панель на порту 3001
|
||||||
|
- Tor-прокси с двумя onion-сервисами (SSH + админка)
|
||||||
- WireGuard VPN для безопасных транзакций
|
- WireGuard VPN для безопасных транзакций
|
||||||
|
|
||||||
## Быстрый старт (одна команда)
|
## Быстрый старт (одна команда)
|
||||||
@@ -61,8 +62,13 @@ curl http://localhost:3001/health
|
|||||||
| `BOT_TOKEN` | ✅ | Токен Telegram бота (@BotFather) |
|
| `BOT_TOKEN` | ✅ | Токен Telegram бота (@BotFather) |
|
||||||
| `ADMIN_IDS` | ✅ | ID администраторов через запятую |
|
| `ADMIN_IDS` | ✅ | ID администраторов через запятую |
|
||||||
| `ENCRYPTION_KEY` | ✅ | Ключ шифрования (32 байта hex) |
|
| `ENCRYPTION_KEY` | ✅ | Ключ шифрования (32 байта hex) |
|
||||||
|
| `ADMIN_SECRET` | ✅ | Секрет для админ-панели |
|
||||||
|
| `ADMIN_PORT` | — | Порт админ-панели (по умолчанию 3001) |
|
||||||
|
| `ADMIN_URL` | — | Полный URL админ-панели (для фото товаров) |
|
||||||
| `SUPER_ADMIN_IDS` | — | ID супер-админов |
|
| `SUPER_ADMIN_IDS` | — | ID супер-админов |
|
||||||
| `SUPPORT_LINK` | — | Ссылка на поддержку |
|
| `SUPPORT_LINK` | — | Ссылка на поддержку |
|
||||||
|
| `SSH_HOST_IP` | — | Куда Tor перенаправляет SSH (по умолчанию host.docker.internal) |
|
||||||
|
| `SHOP_CONTAINER` | — | Имя контейнера магазина (по умолчанию telegram_shop_prod) |
|
||||||
| `WG_ENABLED` | — | `true` / `false` (по умолчанию `false`) |
|
| `WG_ENABLED` | — | `true` / `false` (по умолчанию `false`) |
|
||||||
| `WG_PRIVATE_KEY` | — | Приватный ключ WireGuard |
|
| `WG_PRIVATE_KEY` | — | Приватный ключ WireGuard |
|
||||||
| `WG_PUBLIC_KEY` | — | Публичный ключ WireGuard |
|
| `WG_PUBLIC_KEY` | — | Публичный ключ WireGuard |
|
||||||
@@ -76,6 +82,58 @@ curl http://localhost:3001/health
|
|||||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Tor Proxy
|
||||||
|
|
||||||
|
Проект включает Tor-прокси для доступа к SSH и админ-панели через onion-адреса.
|
||||||
|
|
||||||
|
### Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet → Tor Network → tor-proxy контейнер
|
||||||
|
├── Onion #1 :22 → хост SSH
|
||||||
|
└── Onion #2 :80 → telegram_shop_prod:3001
|
||||||
|
(через Docker сеть tor_proxy_net)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Файлы Tor-прокси
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `tor-proxy/Dockerfile` | Alpine + Tor образ |
|
||||||
|
| `tor-proxy/entrypoint.sh` | Генерация torrc из env vars, валидация, запись onion-адресов |
|
||||||
|
| `tor-proxy/get-onions.sh` | Скрипт чтения onion-адресов и обновления .env |
|
||||||
|
| `tor-proxy/hosts/` | Директория для onion-hosts.txt (bind mount) |
|
||||||
|
|
||||||
|
### После запуска
|
||||||
|
|
||||||
|
Onion-адреса автоматически сохраняются в `tor-proxy/hosts/onion-hosts.txt`. Обновить `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tor-proxy/get-onions.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Вывод:
|
||||||
|
```
|
||||||
|
============================================================
|
||||||
|
Onion services
|
||||||
|
============================================================
|
||||||
|
SSH : xxxxx.onion (port 22 -> host SSH)
|
||||||
|
Admin : yyyyy.onion (port 80 -> telegram_shop_prod:3001)
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
SSH : torify ssh user@xxxxx.onion
|
||||||
|
Admin : open http://yyyyy.onion in Tor Browser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Переменные Tor
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| `SSH_HOST_IP` | `host.docker.internal` | Куда Tor перенаправляет SSH |
|
||||||
|
| `SHOP_CONTAINER` | `telegram_shop_prod` | Контейнер магазина |
|
||||||
|
| `ADMIN_PORT` | `3001` | Порт админки |
|
||||||
|
|
||||||
## Поддерживаемые устройства
|
## Поддерживаемые устройства
|
||||||
|
|
||||||
| Устройство | Архитектура | RAM | Статус |
|
| Устройство | Архитектура | RAM | Статус |
|
||||||
@@ -91,30 +149,31 @@ Docker автоматически собирает нативные модули
|
|||||||
## Архитектура Docker
|
## Архитектура Docker
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────┐
|
||||||
│ telegram_shop_prod (node:22-alpine) │
|
│ docker-compose │
|
||||||
│ ┌──────────────┐ ┌─────────────────────┐ │
|
│ │
|
||||||
│ │ Builder │ │ Runtime │ │
|
│ ┌────────────────────────┐ ┌─────────────────────────┐ │
|
||||||
│ │ python3, g++ │→ │ bash, curl, │ │
|
│ │ telegram_shop_prod │ │ tor-proxy │ │
|
||||||
│ │ make, gcc │ │ wireguard-tools, │ │
|
│ │ (node:22-alpine) │ │ (alpine:3.18 + tor) │ │
|
||||||
│ │ npm install │ │ iptables, bind-tools │ │
|
│ │ │ │ │ │
|
||||||
│ └──────────────┘ └─────────────────────┘ │
|
│ │ Port 3001 │◄─┤ HiddenService :80 │ │
|
||||||
│ Port 3001 (localhost) │
|
│ │ Bot + Admin Panel │ │ HiddenService :22 → SSH│ │
|
||||||
│ Limit: 384MB RAM │
|
│ │ │ │ │ │
|
||||||
└─────────────────────────────────────────────┘
|
│ │ Net: default │ │ Net: default + proxy_net │ │
|
||||||
↑ Volumes: db/, uploads/, .env
|
│ │ + tor_proxy_net │ │ │ │
|
||||||
|
│ └────────────────────────┘ └─────────────────────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ Volumes: Volumes: │
|
||||||
|
│ db/, uploads/, .env tor_data, hosts/ │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Файлы
|
### Сети Docker
|
||||||
|
|
||||||
| Файл | Назначение |
|
| Сеть | Назначение |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Dockerfile` | Multi-stage сборка (builder + runtime) |
|
| `default` | Внутренняя связь между контейнерами |
|
||||||
| `docker-compose.yml` | Конфигурация контейнера |
|
| `tor_proxy_net` | Связь tor-proxy ↔ telegram_shop_prod |
|
||||||
| `install.sh` | Автоматический установщик |
|
|
||||||
| `.dockerignore` | Исключения из Docker-образа |
|
|
||||||
| `.env.example` | Шаблон переменных окружения |
|
|
||||||
| `wg/start.sh` | Скрипт запуска (WireGuard + Node.js) |
|
|
||||||
|
|
||||||
## Команды управления
|
## Команды управления
|
||||||
|
|
||||||
@@ -128,6 +187,10 @@ docker compose up -d --build
|
|||||||
# Логи
|
# Логи
|
||||||
docker compose logs -f
|
docker compose logs -f
|
||||||
|
|
||||||
|
# Логи конкретного сервиса
|
||||||
|
docker compose logs -f tor-proxy
|
||||||
|
docker compose logs -f telegram_shop_prod
|
||||||
|
|
||||||
# Стоп
|
# Стоп
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
@@ -139,6 +202,13 @@ docker compose ps
|
|||||||
|
|
||||||
# Health-check
|
# Health-check
|
||||||
curl http://localhost:3001/health
|
curl http://localhost:3001/health
|
||||||
|
|
||||||
|
# Onion-адреса
|
||||||
|
docker exec tor-proxy cat /var/lib/tor/ssh/hostname
|
||||||
|
docker exec tor-proxy cat /var/lib/tor/admin/hostname
|
||||||
|
|
||||||
|
# Обновить .env с onion-адресами
|
||||||
|
./tor-proxy/get-onions.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## WireGuard
|
## WireGuard
|
||||||
@@ -154,36 +224,56 @@ WireGuard по умолчанию отключен (`WG_ENABLED=false`). Для
|
|||||||
## Безопасность
|
## Безопасность
|
||||||
|
|
||||||
- `.env` монтируется только для чтения (`:ro`)
|
- `.env` монтируется только для чтения (`:ro`)
|
||||||
- Порт 3001 привязан к `127.0.0.1` (доступен только с хоста)
|
- Порт 3001 доступен из LAN и через Tor onion
|
||||||
- Нативные модули компилируются в builder-стейдже (чистый runtime)
|
- Onion-адреса сохраняются в volume (персистентность при перезапуске)
|
||||||
|
- Tor hidden services с валидацией env vars
|
||||||
|
- Нативные модули компилируются в builder-стейдже
|
||||||
- `devDependencies` не попадают в production-образ
|
- `devDependencies` не попадают в production-образ
|
||||||
- Тестовые файлы исключены из Docker-образа (`.dockerignore`)
|
- Тестовые файлы исключены из Docker-образа (`.dockerignore`)
|
||||||
- `node_modules` хоста не попадают в образ (`.dockerignore`)
|
- `node_modules` хоста не попадают в образ (`.dockerignore`)
|
||||||
|
|
||||||
|
## Устойчивость к ошибкам
|
||||||
|
|
||||||
|
- Бот не крашит контейнер при невалидном `BOT_TOKEN`: 5 попыток с задержкой 5с, затем бот отключается, админка продолжает работать
|
||||||
|
- Комиссионные кошельки не обязательны для старта: при отсутствии логируется предупреждение
|
||||||
|
- При потере связи с Telegram API: polling ошибки логируются, процесс продолжает работать
|
||||||
|
|
||||||
## Структура проекта
|
## Структура проекта
|
||||||
|
|
||||||
```
|
```
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── admin/ # Админ-панель (Express)
|
│ ├── admin/ # Админ-панель (Express)
|
||||||
│ ├── config/ # Конфигурация (БД, крипто)
|
│ │ ├── routes/ # Роуты админ-панели
|
||||||
│ ├── context/ # Контекст и состояния бота
|
│ │ ├── views/ # Шаблоны HTML
|
||||||
│ ├── handlers/ # Обработчики команд
|
│ │ ├── public/ # Статические файлы (CSS)
|
||||||
│ ├── middleware/ # Промежуточные обработчики
|
│ │ ├── auth.js # Авторизация
|
||||||
│ ├── migrations/ # Миграции БД
|
│ │ └── server.js # Express-сервер
|
||||||
│ ├── models/ # Модели данных (SQLite)
|
│ ├── config/ # Конфигурация (БД, крипто)
|
||||||
│ ├── router/ # Роутинг Express
|
│ ├── context/ # Контекст и состояния бота
|
||||||
│ ├── services/ # Бизнес-логика
|
│ ├── handlers/ # Обработчики команд
|
||||||
│ ├── utils/ # Утилиты
|
│ │ ├── adminHandlers/ # Обработчики админа
|
||||||
│ └── index.js # Точка входа
|
│ │ └── userHandlers/ # Обработчики пользователя
|
||||||
├── wg/ # WireGuard конфигурация
|
│ ├── middleware/ # Промежуточные обработчики
|
||||||
│ └── start.sh # Скрипт запуска контейнера
|
│ ├── migrations/ # Миграции БД
|
||||||
├── db/ # SQLite база данных (volume)
|
│ ├── models/ # Модели данных
|
||||||
├── uploads/ # Загруженные фото (volume)
|
│ ├── router/ # Роутинг Express
|
||||||
├── Dockerfile # Multi-stage сборка
|
│ ├── services/ # Бизнес-логика
|
||||||
├── docker-compose.yml # Конфигурация контейнера
|
│ ├── utils/ # Утилиты (логирование, валидация, ошибки)
|
||||||
├── install.sh # Установщик (POSIX sh)
|
│ └── index.js # Точка входа
|
||||||
├── .dockerignore # Исключения из образа
|
├── tor-proxy/ # Tor прокси для SSH и админки
|
||||||
├── .env.example # Шаблон переменных
|
│ ├── Dockerfile # Alpine + Tor образ
|
||||||
|
│ ├── entrypoint.sh # Генерация torrc, валидация env vars
|
||||||
|
│ ├── get-onions.sh # Скрипт обновления .env с onion-адресами
|
||||||
|
│ └── hosts/ # Директория для onion-hosts.txt
|
||||||
|
├── wg/ # WireGuard конфигурация
|
||||||
|
│ └── start.sh # Скрипт запуска контейнера
|
||||||
|
├── db/ # SQLite база данных (volume)
|
||||||
|
├── uploads/ # Загруженные фото (volume)
|
||||||
|
├── Dockerfile # Multi-stage сборка магазина
|
||||||
|
├── docker-compose.yml # Конфигурация обоих контейнеров
|
||||||
|
├── install.sh # Установщик (POSIX sh)
|
||||||
|
├── .dockerignore # Исключения из образа
|
||||||
|
├── .env.example # Шаблон переменных
|
||||||
└── package.json
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
39
install.sh
39
install.sh
@@ -256,6 +256,45 @@ else
|
|||||||
printf " %s logs -f\n" "$COMPOSE_CMD"
|
printf " %s logs -f\n" "$COMPOSE_CMD"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 9. Tor Proxy — onion-адреса
|
||||||
|
# ============================================================
|
||||||
|
printf "\n"
|
||||||
|
print_step "Проверка Tor-прокси"
|
||||||
|
|
||||||
|
TOR_RUNNING=0
|
||||||
|
for ATTEMPT in 1 2 3 4 5 6; do
|
||||||
|
sleep 5
|
||||||
|
if docker exec tor-proxy test -s /var/lib/tor/ssh/hostname 2>/dev/null && \
|
||||||
|
docker exec tor-proxy test -s /var/lib/tor/admin/hostname 2>/dev/null; then
|
||||||
|
TOR_RUNNING=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
print_info "Попытка $ATTEMPT/6 — Tor генерирует onion-адреса..."
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$TOR_RUNNING" -eq 1 ]; then
|
||||||
|
SSH_ONION=$(docker exec tor-proxy cat /var/lib/tor/ssh/hostname 2>/dev/null || echo "")
|
||||||
|
ADMIN_ONION=$(docker exec tor-proxy cat /var/lib/tor/admin/hostname 2>/dev/null || echo "")
|
||||||
|
if [ -n "$SSH_ONION" ] && [ -n "$ADMIN_ONION" ]; then
|
||||||
|
print_ok "Tor-прокси работает!"
|
||||||
|
printf "\n ${CYAN}${BOLD}SSH onion:${NC} %s\n" "$SSH_ONION"
|
||||||
|
printf " ${CYAN}${BOLD}Admin onion:${NC} %s\n" "$ADMIN_ONION"
|
||||||
|
printf "\n ${BOLD}Подключение:${NC}\n"
|
||||||
|
printf " SSH: torify ssh root@%s\n" "$SSH_ONION"
|
||||||
|
printf " Admin: откройте http://%s в Tor Browser\n" "$ADMIN_ONION"
|
||||||
|
|
||||||
|
if [ -f "./tor-proxy/get-onions.sh" ]; then
|
||||||
|
sh ./tor-proxy/get-onions.sh 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warn "Tor запущен, но onion-адреса не найдены"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warn "Tor-прокси не стартовал за 30 секунд. Проверьте логи:"
|
||||||
|
printf " docker logs tor-proxy\n"
|
||||||
|
fi
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Готово
|
# Готово
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -62,15 +62,17 @@ export default class ViewHandler {
|
|||||||
let hiddenPhotoMessage;
|
let hiddenPhotoMessage;
|
||||||
|
|
||||||
if (product.photo_url) {
|
if (product.photo_url) {
|
||||||
|
const photoUrl = product.photo_url.startsWith('http') ? product.photo_url : `${process.env.ADMIN_URL}${product.photo_url}`;
|
||||||
try {
|
try {
|
||||||
photoMessage = await bot.sendPhoto(chatId, product.photo_url, {caption: 'Public photo'});
|
photoMessage = await bot.sendPhoto(chatId, photoUrl, {caption: 'Public photo'});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'})
|
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Public photo'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (product.hidden_photo_url) {
|
if (product.hidden_photo_url) {
|
||||||
|
const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`;
|
||||||
try {
|
try {
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, {caption: 'Hidden photo'});
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, {caption: 'Hidden photo'});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", {caption: 'Hidden photo'})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,9 +356,11 @@ export default class UserProductHandler {
|
|||||||
// Отправляем фото, если оно существует
|
// Отправляем фото, если оно существует
|
||||||
let photoMessage;
|
let photoMessage;
|
||||||
if (product.photo_url) {
|
if (product.photo_url) {
|
||||||
|
const photoUrl = product.photo_url.startsWith('http') ? product.photo_url : `${process.env.ADMIN_URL}${product.photo_url}`;
|
||||||
try {
|
try {
|
||||||
photoMessage = await bot.sendPhoto(chatId, product.photo_url, { caption: 'Public photo' });
|
photoMessage = await bot.sendPhoto(chatId, photoUrl, { caption: 'Public photo' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.warn({ err: e }, 'Failed to send product photo');
|
||||||
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Public photo' });
|
photoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Public photo' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,9 +708,11 @@ export default class UserProductHandler {
|
|||||||
// Отправляем Hidden Photo
|
// Отправляем Hidden Photo
|
||||||
let hiddenPhotoMessage;
|
let hiddenPhotoMessage;
|
||||||
if (product.hidden_photo_url) {
|
if (product.hidden_photo_url) {
|
||||||
|
const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`;
|
||||||
try {
|
try {
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, { caption: 'Hidden photo' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.warn({ err: e }, 'Failed to send hidden photo');
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,8 +180,9 @@ export default class UserPurchaseHandler {
|
|||||||
// Отправляем Hidden Photo
|
// Отправляем Hidden Photo
|
||||||
let hiddenPhotoMessage;
|
let hiddenPhotoMessage;
|
||||||
if (product.hidden_photo_url) {
|
if (product.hidden_photo_url) {
|
||||||
|
const hiddenPhotoUrl = product.hidden_photo_url.startsWith('http') ? product.hidden_photo_url : `${process.env.ADMIN_URL}${product.hidden_photo_url}`;
|
||||||
try {
|
try {
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, product.hidden_photo_url, { caption: 'Hidden photo' });
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, hiddenPhotoUrl, { caption: 'Hidden photo' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
|
hiddenPhotoMessage = await bot.sendPhoto(chatId, "./corrupt-photo.jpg", { caption: 'Hidden photo' });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user