Photo downloads had zero retries, unlike video (which already retries stalls/timeouts). Confirmed live: a photo whose download hit "500" from VK's own CDN (sun9-87.userapi.com) was back to a normal 200 moments later - a one-off hiccup, not a real problem with the photo - but with no retry, it went straight to the link-only fallback for good, since a published post is never revisited. Now retries up to 2 more times (3s apart) on a 5xx status, timeout, or connection error; a 4xx (permanently gone/bad URL) still fails immediately since retrying that wouldn't help. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RedAirsoft VK to Telegram & MAX Messenger Poster
Автоматизированный Docker-сервис для периодического парсинга новых постов из сообщества ВКонтакте (Red Airsoft | Страйкбол) и их одновременной публикации в Telegram и мессенджер МАКС (MAX Messenger) с отчётами администраторам.
📌 Инфраструктура и Деплой
Актуальный способ деплоя — Coolify, автодеплой по пушу в gitea/main. Старый ручной способ (checkout в /opt/redairsoft_poster/ + docker-compose руками через Proxmox, описанный ниже архивно) больше не используется — тот контейнер (redairsoft-vk-poster) был удалён 2026-08-15 как заброшенный.
| Параметр | Значение |
|---|---|
| Деплой | Coolify, приложение "RedAirsoft Poster", сервер redairsoft-poster-lxc |
| URL | http://hjbmqj1ohievdrebpltnzex0.192.168.1.106.sslip.io |
| Хост контейнера | Тот же LXC 107 (192.168.1.106) — Coolify управляет им напрямую, не через /opt/redairsoft_poster/ |
| Имя Docker-контейнера | Динамическое, вида <coolify-app-id>-<deploy-id>, тег образа = хэш закоммиченного коммита. Смотреть через docker ps — актуальный это Up, с тегом = последний git log хэш |
| Gitea Репозиторий | http://192.168.1.135:3000/exostring/redairsoft_vk_gt_max_sender |
| Хост Proxmox (для ручной диагностики) | 192.168.1.222 (root SSH через ключ ~/.ssh/id_ed25519_proxmox) |
| Локальный Telegram Bot API | Встроен в контейнер (http://127.0.0.1:8081), поддерживает загрузку видео до 2 ГБ |
| База данных | SQLite /app/data/poster.db внутри контейнера (персистентный том) |
Диагностика через SSH (read-only)
# Найти актуальный контейнер (смотреть на тег образа = свежий commit hash):
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- docker ps -a'
# Логи актуального контейнера:
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- docker logs --tail 100 <имя-контейнера>'
Деплой новой версии — просто git push в gitea/main, Coolify подхватывает автоматически.
⚙️ Как работает сервис
- Донор→рецепиент маршруты (
data/routes.json):- Сервис может опрашивать сразу несколько групп ВК ("доноров"), и для каждой отдельно настроено, в какие Telegram и МАКС чаты публиковать ("рецепиенты"), плюс независимые вкл/выкл для TG и МАКС на каждом маршруте.
- Список маршрутов хранится в
data/routes.json(персистентный том, не в git). Если файла нет — при первом запуске он создаётся автоматически из старых.env-переменныхVK_SOURCE/TG_CHAT_ID/MAX_CHAT_ID(один маршрут сid: "default"), так что апгрейд с однo-группового режима ничего не ломает. - Пример формата —
routes.json.exampleв корне репозитория. Правкаroutes.jsonтребует перезапуска контейнера, чтобы изменения подхватились. - Маршруты в рамках одного цикла опроса обрабатываются строго последовательно (без параллелизма) — это защищает от флуда на VK API, загрузке медиа в Telegram/локальный Bot API и отправке в МАКС, поскольку токены ботов общие на все маршруты.
- Опрос стены ВКонтакте:
- Каждые 15 минут (
CHECK_INTERVAL_MINUTES) делает запрос к методуwall.getдля каждой группы изroutes.json. - Фильтрует посты автора (без репостов и рекламы сторонних сообществ).
- Каждые 15 минут (
- Защита от спама и режим запуска (
BOOTSTRAP_MODE):- Установлен режим
BOOTSTRAP_MODE=skip_existing. - При первом запуске или перезапуске сервис запоминает текущие посты стены и ничего не публикует, пока в ВК не выйдет новый пост.
- Установлен режим
- Загрузка медиа и поддержка длинных видео:
- Картинки скачиваются в максимальном разрешении.
- Видео загружается через
yt-dlpв качестве до 720p (длительностью до 2 часов и размером до 2000 МБ). - Встроенный в контейнер локальный
telegram-bot-apiзагружает тяжелые видеофайлы напрямую в Telegram без ограничений облачного API (20-50 МБ).
- Форматирование текста:
- Ссылки ВКонтакте вида
[club123|Название]и[id123|Имя]преобразуются в корректные кликабельные HTML-ссылки. - Первый абзац / заголовок выделяется жирным шрифтом
<b>...</b>. - Удаляются разделительные полосы (
━━━,───) и лишние переносы строк.
- Ссылки ВКонтакте вида
- Публикация в мессенджер МАКС:
- Загрузка медиа в МАКС через
/uploads?type=photoи/uploads?type=video. - Поллинг готовности видео (
/videos/{token}) перед публикацией сообщения.
- Загрузка медиа в МАКС через
- Отчёты администраторам:
- После успешной публикации или при ошибке бот отправляет отчёт со ссылками на оригинальный пост в ВК, Telegram и МАКС в личные сообщения администраторам (
TG_ADMIN_IDS).
- После успешной публикации или при ошибке бот отправляет отчёт со ссылками на оригинальный пост в ВК, Telegram и МАКС в личные сообщения администраторам (
- Очистка диска и кэша:
- Временные файлы удаляются сразу после публикации.
- Фоновый воркер
cleaner.pyкаждые 15 минут удаляет старые файлы из/tmp/poster_cache, гарантируя защиту активных скачиваний через реестр локов.
🔀 Маршруты донор→рецепиент (data/routes.json)
Формат одного маршрута:
{
"id": "redairsoft_main",
"name": "Red Airsoft (основная группа)",
"vk_source": "public36860851",
"tg_chat_id": "-1001303630155",
"tg_enabled": true,
"max_chat_id": "123456",
"max_enabled": true,
"stale_alert_enabled": true,
"stale_alert_ids": [123456789]
}
| Поле | Описание |
|---|---|
id |
Уникальный технический идентификатор маршрута (используется в БД и логах) |
name |
Человекочитаемое имя для отчётов администраторам |
vk_source |
Группа-донор ВК: screen name, URL или owner_id |
tg_chat_id / max_chat_id |
Куда публиковать (chat/channel ID) |
tg_enabled / max_enabled |
Переключатель — публиковать ли в эту платформу для этого маршрута (только TG, только МАКС, или оба) |
stale_alert_enabled |
Оповещать ли, если донор давно не постил (см. ниже). По умолчанию false |
stale_alert_ids |
Кому слать оповещение о простое (список TG ID). Пусто/не указано → шлётся TG_ADMIN_IDS |
Можно завести несколько маршрутов с разными vk_source, каждый — в свои TG/МАКС чаты. Токены ботов (TG_BOT_TOKEN, MAX_BOT_TOKEN, VK_ACCESS_TOKEN) общие на все маршруты — один бот пишет в разные чаты.
Оповещение о "молчащем" доноре
Глобальный порог — STALE_DONOR_ALERT_DAYS в .env (0 = функция выключена целиком). Если у конкретного маршрута stale_alert_enabled: true и донор не постил дольше порога — уходит одно сообщение получателям из stale_alert_ids (или TG_ADMIN_IDS, если список пуст). Не спамит: пока донор молчит, повторно не напоминает; как только появляется новый пост — оповещение автоматически "перевзводится" для следующего простоя. Если бот не может достучаться до кого-то из stale_alert_ids (человек ни разу не писал боту) — отдельным сообщением получают TG_ADMIN_IDS.
Узнать свой Telegram ID
Написать боту /id в личку — ответит вашим ID в копируемом виде (<code>...</code>), чтобы вставить в TG_ADMIN_IDS или stale_alert_ids.
vk_source принимает screen name, полный URL (vk.com и vk.ru) или owner_id — можно указывать как есть, без ручной нормализации.
Файл — обычный JSON, но допускает построчные комментарии // текст, чтобы подписывать, где какой маршрут (полноценных JSON-комментариев не существует, здесь это добавлено отдельно — строка, у которой после пробелов идёт //, вырезается перед парсингом):
[
// redairsoft
{
"id": "redairsoft",
"vk_source": "public36860851",
"tg_chat_id": "-1001303630155",
"max_chat_id": "-69722432869632"
},
// strike_expo
{
"id": "strike_expo",
"vk_source": "https://vk.ru/strike_expo",
"tg_chat_id": "-1003099077190",
"max_chat_id": "-77898705339648"
}
]
🔑 Конфигурация (.env)
| Переменная | Описание |
|---|---|
VK_ACCESS_TOKEN |
Сервисный токен приложения ВК (d0a64...), общий на все маршруты |
ROUTES_CONFIG_PATH |
Путь к файлу маршрутов, по умолчанию data/routes.json |
VK_SOURCE / TG_CHAT_ID / MAX_CHAT_ID |
Легаси fallback: используются только для авто-генерации первого маршрута, если routes.json ещё не существует |
TG_BOT_TOKEN |
Токен Telegram бота (от @BotFather), общий на все маршруты |
TG_MEDIA_CHANNEL_ID |
Скрытый канал-хранилище (опционально) |
TG_ADMIN_IDS |
Telegram ID админов через запятую (например 442509142) |
LOCAL_BOT_API_URL |
http://127.0.0.1:8081 (встроен в контейнер) |
MAX_BOT_TOKEN |
Токен бота в мессенджере МАКС, общий на все маршруты |
BOOTSTRAP_MODE |
skip_existing |
CHECK_INTERVAL_MINUTES |
15 |
VIDEO_MAX_DURATION_SEC |
7200 (2 часа) |
VIDEO_MAX_SIZE_MB_LOCAL |
2000 (2 ГБ) |
📁 Структура исходного кода
├── src/
│ ├── config.py # Настройки Pydantic Settings
│ ├── routes.py # Загрузка/валидация data/routes.json (донор->рецепиент маршруты)
│ ├── database.py # Хранилище SQLite (посты, статусы по route_id, даты)
│ ├── vk_client.py # Клиент VK API (wall.get, извлечение медиа)
│ ├── media_processor.py # Загрузка фото/видео (yt-dlp, aiohttp)
│ ├── text_formatter.py # Конвертер ссылок ВК, очистка текста, HTML
│ ├── tg_poster.py # Отправка в Telegram (Rich Message + Local API)
│ ├── max_poster.py # Отправка в MAX Messenger (Uploads + Polling)
│ ├── cleaner.py # Фоновая очистка временных файлов
│ ├── admin_notifier.py # Агрегированные отчёты администраторам по всем маршрутам
│ └── main.py # Точка входа, цикл опроса по всем маршрутам
├── Dockerfile # Multi-stage образ: aiogram/telegram-bot-api + python:3.12-slim + ffmpeg
├── docker-compose.yml # Конфигурация запуска сервиса
├── docker-entrypoint.sh # Автозапуск локального Telegram Bot API + постера
├── requirements.txt # Python зависимости (aiogram, yt-dlp, loguru, aiohttp, pydantic)
├── routes.json.example # Пример формата data/routes.json
└── .env # Переменные окружения