Add multi-route donor->recipient support, fix VK hashtag truncation

Posts can now be sourced from multiple VK groups, each routed to its own
Telegram/MAX destination(s) with independent on/off switches, configured
via data/routes.json (supports // line comments). Falls back to a single
route auto-generated from the legacy VK_SOURCE/TG_CHAT_ID/MAX_CHAT_ID env
vars if routes.json doesn't exist yet, so existing deployments keep working.

Routes are processed strictly sequentially within a cycle (no concurrency)
to keep flood control on VK/TG/MAX correct, since bot tokens are shared
across routes. DB schema gains route_id in the posts uniqueness key so the
same VK donor can safely feed multiple routes without status collisions.

Also removes the trailing-hashtag-stripping logic in text_formatter, which
was silently deleting VK posts' own hashtags whenever COMMON_TAGS wasn't
configured (it always wasn't) - posts are now forwarded unchanged.
This commit is contained in:
2026-08-18 14:48:22 +05:00
parent ec65aaf57d
commit 20135263c4
11 changed files with 596 additions and 233 deletions
+85 -35
View File
@@ -6,47 +6,42 @@
## 📌 Инфраструктура и Деплой
**Актуальный способ деплоя — Coolify**, автодеплой по пушу в `gitea/main`. Старый ручной способ (checkout в `/opt/redairsoft_poster/` + `docker-compose` руками через Proxmox, описанный ниже архивно) больше не используется — тот контейнер (`redairsoft-vk-poster`) был удалён 2026-08-15 как заброшенный.
| Параметр | Значение |
|---|---|
| **Хост Proxmox** | `192.168.1.222` (root SSH через ключ `~/.ssh/id_ed25519_proxmox`) |
| **LXC Контейнер** | **CT 107** (`redairsoft-poster`, IP: `192.168.1.106`, Debian 12) |
| **Ресурсы LXC** | 2 vCPU, 1.5 GB RAM, 512 MB Swap, 16 GB Disk, `onboot=1` |
| **Деплой** | 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` |
| **Gitea SSH Remote** | `ssh://git@192.168.1.135:2222/exostring/redairsoft_vk_gt_max_sender.git` |
| **Путь проекта на LXC 107** | `/opt/redairsoft_poster/` |
| **Имя Docker контейнера** | `redairsoft-vk-poster` |
| **Хост Proxmox** (для ручной диагностики) | `192.168.1.222` (root SSH через ключ `~/.ssh/id_ed25519_proxmox`) |
| **Локальный Telegram Bot API** | Встроен в контейнер (`http://127.0.0.1:8081`), поддерживает загрузку видео до 2 ГБ |
| **База данных** | SQLite `/opt/redairsoft_poster/data/poster.db` (персистентный том Docker `./data`) |
| **База данных** | SQLite `/app/data/poster.db` внутри контейнера (персистентный том) |
---
## 🚀 Управление проектом (Команды из консоли / PowerShell)
Все команды выполняются через Proxmox хост:
### Диагностика через SSH (read-only)
```bash
# 1. Посмотреть статус и логи бота:
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- docker logs --tail 50 redairsoft-vk-poster'
# Найти актуальный контейнер (смотреть на тег образа = свежий commit hash):
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- docker ps -a'
# 2. Логи в реальном времени (follow):
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- docker logs -f redairsoft-vk-poster'
# 3. Запустить проект:
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- bash -c "cd /opt/redairsoft_poster && docker-compose up -d"'
# 4. Остановить проект:
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- bash -c "cd /opt/redairsoft_poster && docker-compose stop"'
# 5. Обновить из Gitea и пересобрать:
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- bash -c "cd /opt/redairsoft_poster && git pull origin main && docker-compose build && docker-compose up -d"'
# Логи актуального контейнера:
ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- docker logs --tail 100 <имя-контейнера>'
```
Деплой новой версии — просто `git push` в `gitea/main`, Coolify подхватывает автоматически.
---
## ⚙️ Как работает сервис
0. **Донор→рецепиент маршруты (`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.example) в корне репозитория. Правка `routes.json` требует перезапуска контейнера, чтобы изменения подхватились.
- Маршруты в рамках одного цикла опроса обрабатываются **строго последовательно** (без параллелизма) — это защищает от флуда на VK API, загрузке медиа в Telegram/локальный Bot API и отправке в МАКС, поскольку токены ботов общие на все маршруты.
1. **Опрос стены ВКонтакте**:
- Каждые 15 минут делает запрос к методу `wall.get` для группы `public36860851` (`owner_id: -36860851`).
- Каждые 15 минут (`CHECK_INTERVAL_MINUTES`) делает запрос к методу `wall.get` для каждой группы из `routes.json`.
- Фильтрует посты автора (без репостов и рекламы сторонних сообществ).
2. **Защита от спама и режим запуска (`BOOTSTRAP_MODE`)**:
- Установлен режим `BOOTSTRAP_MODE=skip_existing`.
@@ -70,19 +65,72 @@ ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- bash -c "cd
---
## 🔀 Маршруты донор→рецепиент (`data/routes.json`)
Формат одного маршрута:
```json
{
"id": "redairsoft_main",
"name": "Red Airsoft (основная группа)",
"vk_source": "public36860851",
"tg_chat_id": "-1001303630155",
"tg_enabled": true,
"max_chat_id": "123456",
"max_enabled": true
}
```
| Поле | Описание |
|---|---|
| `id` | Уникальный технический идентификатор маршрута (используется в БД и логах) |
| `name` | Человекочитаемое имя для отчётов администраторам |
| `vk_source` | Группа-донор ВК: screen name, URL или owner_id |
| `tg_chat_id` / `max_chat_id` | Куда публиковать (chat/channel ID) |
| `tg_enabled` / `max_enabled` | Переключатель — публиковать ли в эту платформу для этого маршрута (только TG, только МАКС, или оба) |
Можно завести несколько маршрутов с разными `vk_source`, каждый — в свои TG/МАКС чаты. Токены ботов (`TG_BOT_TOKEN`, `MAX_BOT_TOKEN`, `VK_ACCESS_TOKEN`) общие на все маршруты — один бот пишет в разные чаты.
`vk_source` принимает screen name, полный URL (`vk.com` и `vk.ru`) или `owner_id` — можно указывать как есть, без ручной нормализации.
Файл — обычный JSON, но допускает построчные комментарии `// текст`, чтобы подписывать, где какой маршрут (полноценных JSON-комментариев не существует, здесь это добавлено отдельно — строка, у которой после пробелов идёт `//`, вырезается перед парсингом):
```jsonc
[
// 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...`) |
| `VK_SOURCE` | `public36860851` |
| `TG_BOT_TOKEN` | Токен Telegram бота (от `@BotFather`) |
| `TG_CHAT_ID` | ID канала Telegram (например `-1001303630155`) |
| `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` | Токен бота в мессенджере МАКС |
| `MAX_CHAT_ID` | ID чата в МАКС |
| `MAX_BOT_TOKEN` | Токен бота в мессенджере МАКС, общий на все маршруты |
| `BOOTSTRAP_MODE` | `skip_existing` |
| `CHECK_INTERVAL_MINUTES` | `15` |
| `VIDEO_MAX_DURATION_SEC` | `7200` (2 часа) |
@@ -95,18 +143,20 @@ ssh -i ~/.ssh/id_ed25519_proxmox root@192.168.1.222 'pct exec 107 -- bash -c "cd
```
├── src/
│ ├── config.py # Настройки Pydantic Settings
│ ├── database.py # Хранилище SQLite (посты, статусы, даты)
│ ├── 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 # Точка входа и главный цикл опроса
│ ├── 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 # Переменные окружения
```