Есть моменты в разработке, когда решение приходит не через усложнение, а через упрощение. И тогда понимаешь: вот оно, правильное.
Контекст: система, которая «почти» работала
У нас есть бот-архивариус для сообщества СуккоГрадъ. Он принимает вебхуки из MAX, сохраняет посты, комментарии, сообщения чатов в БД, публикует в Telegram и на YouTube. Классический «швейцарский нож» для сообщества.
Архитектура:
- MAX — источник событий (посты, комментарии, чаты).
- PHP-приёмник на сайте — ловит вебхуки, кладёт в
queue.jsonl. - Python-воркеры — читают очередь, обрабатывают, публикуют.
Долгое время всё работало. Но была одна проблема — синхронизация. Это «догоняющий механизм» на случай, если бот был выключен и вебхук пропущен. Он ходит в MAX API, тянет пропущенные посты и сохраняет в БД.
Проблема: два пути — две логики
Мы недавно переделали обработку вебхуков — сделали её правильной:
attachmentsсохраняются сразу (raw JSON).media_status='pending'— маркер «надо скачать».markup,channel_title, теги — заполняются.- Медиа скачивает отдельный воркер, а не обработчик вебхука.
- Футер добавляется через
processor— отдельный сервис.
Но sync остался старым. Он работал по логике «до рефакторинга»:
- Сохранял пост напрямую в БД — без
media_status. - Скачивал видео сам — в том же процессе.
- Пытался править пост через API — чтобы добавить футер.
И тут вылезла проблема. Пост, который прошёл через sync, отличался от поста, который пришёл через вебхук:
channel_title = NULL.markup = NULL.media_status = NULL.attachments— в другом формате.
Одна и та же сущность, но два разных пути. Классический архитектурный разрыв.
Тупик: попытка «починить» через API
Логичным казалось: «Давайте sync будет работать так же, как вебхук — но через API». То есть:
syncнаходит пропущенный пост.- Правит его через API — чтобы MAX прислал
message_edited. handle_editподхватывает — и обновляет БД.
Проверили на живом посте. Бот может редактировать чужие посты. MAX присылает message_edited. Всё сработало.
Но при этом сломали видео.
Смотрим вебхук, который пришёл после правки:
"attachments": [
{
"type": "share", ← было "video"
"payload": {"url": "http://vk.ru/..."}
}
]
MAX пересобрал attachments. Увидел в тексте ссылку vk.ru/holidayhousesukko — заменил видео на превью ссылки. Видео исчезло.
Это был провал. Мы пытались «починить» систему, а сломали реальный пост.
Момент прозрения: «как футер»
Тогда вы сказали:
«Осталось реализовать синхронизацию через вебхук. Так, как это реализовано в добавлении футера».
И это была точка поворота.
Смотрим, как работает футер — и почему это правильно:
handle— сохраняет пост в БД (rawattachments,media_status='pending').processor— берёт посты сprocessed=0.- Правит пост через API — добавляет футер.
- MAX присылает
message_edited. handle_edit— обновляетtextиmarkupв БД.processed=1— только после успеха.
Ключевое: processor не «синхронизирует» пост. Он правит уже существующий пост. А вебхук message_edited завершает цикл — обновляет БД.
Механизм элегантный:
- Правка — через API.
- Синхронизация БД — через вебхук.
- Никакого дублирования логики.
Вопрос: а что если sync должен работать так же — не сам сохранять пост, а «притвориться вебхуком»?
Решение: sync пишет в queue.jsonl
Идея настолько простая, что её легко упустить:
Пусть
syncпишет не в БД, а в ту же очередь, что и PHP-приёмник.
Схема:
sync → нашёл пропущенный пост в MAX
→ сформировал JSON в формате вебхука
→ записал в queue.jsonl
↓
queue_reader (в sukko-ingest)
→ читает queue.jsonl
→ передаёт в dispatcher.process_webhook_event
↓
dispatcher → MaxChannelHandler.handle
→ сохраняет пост с raw attachments, media_status='pending', markup, channel_title
↓
media_downloader → скачивает видео → media_status='ready'
processor → добавляет футер → MAX шлёт message_edited → handle_edit обновляет БД → processed=1
↓
publisher_tg → публикует в TG
publisher_yt → заливает на YouTube
Что изменилось:
syncбольше не сохраняет в БД.syncбольше не скачивает медиа.syncбольше не правит посты через API.syncпросто пишет в очередь.
Всё остальное делают штатные сервисы. Один путь — одна логика.
Почему это просто
1. Одна точка входа. PHP пишет в queue.jsonl. И sync тоже. queue_reader не различает. Одинаковая обработка.
2. Нет дублирования. handle — единственное место, где сохраняется пост. processor — единственное, где добавляется футер. media-downloader — единственное, где скачивается видео.
3. Идемпотентность. Если пост уже в БД — handle не создаст дубль. get_existing_post_ids + ON DUPLICATE KEY UPDATE.
4. Прозрачность. sync стал маленьким — ~50 строк вместо ~500. Легко читать, легко тестировать.
5. Надёжность. queue.jsonl — единая точка отказа. Если что-то упадёт — видно где. Никаких «двух путей».
Почему казалось, что нужен API
Соблазн был велик: «раз MAX умеет редактировать — давайте через API». Но:
edit_messageменяет attachments — MAX пересобирает их.- Риск потери медиа — высокий.
- Логика раздваивается — вебхук отдельно, API отдельно.
API — мощный инструмент. Но он не должен быть «обходным путём» для синхронизации. Вебхук — вот правильный путь.
Мораль
«Гениальное» решение через API — казалось умным:
- Правка поста → событие → синхронизация.
«Простое» решение через очередь — казалось тривиальным:
- Пишем в очередь → всё работает.
Но именно простое победило:
- Меньше кода.
- Меньше путей.
- Меньше ошибок.
- Единая логика.
Эпилог
Когда-то кто-то сказал: «Совершенство достигнуто не тогда, когда нечего добавить, а когда нечего убрать».
Мы убрали из sync:
- Сохранение в БД.
- Скачивание медиа.
- Правку через API.
- Работу с тегами.
Осталось:
- Найти пропущенные посты.
- Записать их в очередь.
И всё заработало так же, как для вебхука. Потому что это и есть вебхук — просто созданный нами, а не MAX-ом.
P.S. А пост 733, который мы сломали, вы удалили. Это тоже урок: не все эксперименты удаётся откатить. Иногда — приходится признать потерю и двигаться дальше. Но ценность урока — выше потери одного поста.