Фундаментальное ограничение: нет официального API
У WhatsApp нет публичного API для личных аккаунтов. Официальный WhatsApp Business Cloud API работает только для бизнес-аккаунтов и не умеет читать каналы - он лишь получает сообщения, которые пользователи сами отправляют боту.
Для чтения публичных каналов и групп единственный практичный путь - автоматизация WhatsApp Web через библиотеку whatsapp-web.js. Она запускает headless Chromium через Puppeteer и взаимодействует с внутренним JavaScript API WhatsApp Web по WebSocket.
Важная оговорка: whatsapp-web.js - реверс-инженерное решение. Meta периодически меняет внутренние API, и библиотека ломается, поэтому нужно закладывать время на её обслуживание.
Каналы vs группы: принципиальная разница
Группы (chat.isGroup === true) требуют членства в чате, метод fetchMessages() у них работает стабильно, а риск бана - средний. Каналы (chat.isChannel === true) требуют подписки, fetchMessages() у них работает экспериментально, события в реальном времени нестабильны, зато риск бана ниже. На практике большинство организаторов городских событий пока используют обычные группы, а не более новые каналы.
Инициализация клиента и стратегия аутентификации
Первый запуск требует сканирования QR-кода через функцию WhatsApp "Связанные устройства". Дальнейшая работа опирается на LocalAuth, которая сохраняет профиль и ключи шифрования локально.
- первый запуск - QR-код появляется в терминале;
- сохранение сессии - LocalAuth хранит профиль в указанной папке;
- истечение сессии - сессия падает при отключении телефона или ручном выходе;
- Docker/VPS - папку сессии стоит монтировать как volume, чтобы не сканировать QR заново при каждом перезапуске.
Два режима работы парсера WhatsApp
Режим A - слушатель в реальном времени: процесс работает постоянно и реагирует на новые сообщения через client.on('message'). Это оптимальный вариант для продакшена.
Режим B - пакетное извлечение: запускается по расписанию (cron или планировщик задач), забирает сообщения с момента последней проверки и завершает работу.
Важное ограничение: fetchMessages({ limit: N }) возвращает максимум последние N сообщений из кэша, без постраничной подгрузки. Получить историю глубже кэша не получится.
Фильтрация чатов по названию
Лучше использовать частичное совпадение по ключевым словам, а не точное название чата - названия чатов со временем меняются: например, ключи вроде "ивент", "event", "афиша", "концерт", "выставка". Дополнительно стоит вести белый список ID чатов - он остается стабильным даже после переименования группы.
Слой AI: OpenAI как классификатор и структуратор
OpenAI решает две задачи: определяет, является ли сообщение анонсом события, и извлекает структурированные данные в JSON - название, дату, время, место, описание, категорию, цену, контакт.
Системный промпт должен включать текущую дату для разбора относительных дат ("в эту пятницу" превращается в конкретную дату), перечислять явные критерии отклонения, использовать JSON-режим для гарантированно валидного вывода, температуру 0.2 для детерминированной классификации и модель gpt-4o-mini ради экономии.
Дедупликация: одно событие - много репостов
Уровень 1 - сравнение строк по схожести (порог от 72%) с недавними сообщениями. Уровень 2 - семантическая дедупликация: сравнение структурированных полей события (дата плюс место плюс схожесть названия).
Хранение данных: Supabase как бэкенд
База данных PostgreSQL хранит события с полями: title, date, time, location, description, category, price, contact, source_chat, raw_text, created_at, is_approved. Важно всегда включать поле is_approved и требовать модерацию человеком перед публикацией.
Обработка временных меток
WhatsApp возвращает Unix timestamp в секундах, а JavaScript работает с миллисекундами - значение нужно умножать на 1000. Часовой пояс всегда стоит указывать явно и в промптах, и в операциях с базой данных.
Rate limiting и защита от бана
- пауза 0.5-1 секунда между вызовами fetchMessages();
- никаких автоматических ответов - это главный риск бана;
- случайный джиттер в задержках;
- отдельный номер телефона для парсера;
- телефон должен оставаться онлайн постоянно.
Персистентность состояния между запусками
Время последней проверки стоит хранить в отдельном JSON-файле состояния - не в папке скрипта, а, например, в %APPDATA% на Windows или $HOME на Linux, - чтобы не обрабатывать одни и те же сообщения повторно.
Уведомления о новых ивентах: Telegram-бот
По каждому найденному событию, ожидающему одобрения, имеет смысл отправлять уведомление в Telegram со ссылкой на панель модерации.
Проблема Puppeteer/Chromium в продакшене
Загрузка Chromium (около 150-170 МБ) создает сложности при деплое: долгая первая установка npm, потребление Chrome 200-400 МБ оперативной памяти, необходимость флагов --no-sandbox и --disable-setuid-sandbox на Linux, а в Docker - пакет chromium-browser из apt.
Чеклист запуска
- Node.js - установить версию 18+, версия 16 не поддерживается;
- npm install - автоматически скачивает Chromium, около 150 МБ, нужен стабильный интернет;
- .env - хранить API-ключи, не коммитить в git;
- первый запуск - отсканировать QR-код, телефон должен оставаться онлайн;
- сессия - проверить персистентность, использовать Docker volume, не удалять папку сессии;
- расписание - cron или планировщик задач, оптимально раз в 2-4 часа;
- мониторинг - уведомление в Telegram при auth_failure, сессия может упасть без предупреждения;
- модерация - админ-панель, никогда не публиковать автоматически без проверки.
Общая архитектура системы
Пайплайн выглядит так: группы WhatsApp - whatsapp-web.js - фильтр чатов - фильтр дублей - классификация OpenAI - Supabase (неподтвержденные записи) - уведомление в Telegram - ручная модерация - сайт событий.
Фундаментальный риск архитектуры
Вся система зависит от того, что WhatsApp Web остается доступным и whatsapp-web.js продолжает работать. Meta непредсказуемо меняет внутренние протоколы и может намеренно усложнять реверс-инжиниринг.
Для продакшена стоит мониторить события auth_failure и disconnected, регулярно обновлять whatsapp-web.js, держать резервные источники данных (каналы Telegram, Instagram, сайты городов) и рассматривать WhatsApp как один из источников, а не единственный.