Расписания и Подписки: концепция и реализация¶
Документ описывает идею и реализуемые фичи подсистемы расписаний рассылок ("Подписки") для доставки контента в каналы/чаты (в первую очередь Telegram), включая модель данных, API, алгоритмы планировщика и требования к UI/UX. Содержит сводку договорённостей из текущего репозитория, CHANGELOG/TODO, UI_TODO и консолидацию идей из диалога про «Сеты категорий и расписания».
Цели¶
- Гибко планировать отправку контента подписчикам по расписанию или по триггеру.
- Отбирать релевантный контент по правилам категорий (в т.ч. предустановленные «Сеты»), избегать повторов.
- Давать редакторам удобный UI для создания и правки подписок, с превью ближайших запусков и логами.
- Обеспечивать предсказуемую нагрузку и прозрачные ограничения (лимиты/тихие часы/ретраи).
Ключевые сущности¶
Post— медиа‑объект с метаданными и массивом категорийsource.category.ContentRule— (планируется) правило отбора контента (что отправлять):- Дерево условий категорий (AST) с узлами
AND | OR | NOT | LEAF. - Поддержка пресетов (избранные «Сеты»), флаг
isPresetдля быстрого списка. - Текущее состояние: используется include/exclude/all списки категорий из
Subscribe.options. Subscribe— подписка (кому и когда отправлять):- Получатель
peer { provider, type, id }(например: TG пользователь/чат, в перспективе VK и др.). - Опции
options(тип рассылки, лимиты, привязанныйcontentRule, и пр.). - Периодичность
periodicity/periodicity_v2(расписание/триггер, см. ниже). - Служебные поля:
enabled,last_fire,next_fire,timestamps. PostDelivery— история отправок (для исключения повторов по подписке).SubscribeLog— логи срабатываний/ошибок (для просмотра в UI).
Правила контента (DSL)¶
Правила категорий хранятся в виде AST, позволяя выражать композиции AND/OR/NOT над множествами категорий. Пример JSON‑DSL:
{ "type": "AND", "children": [
{ "type": "LEAF", "categories": ["humor"] },
{ "type": "NOT", "children": [ { "type": "LEAF", "categories": ["18plus"] } ] }
]}
Трансляция AST в Mongo делается через $expr и сет‑операции:
- LEAF с одной категорией → { $in: ["cat", "$source.category"] }
- LEAF с несколькими → { $setIsSubset: [[...cats], "$source.category"] }
- AND/OR → { $and: [...] }/{ $or: [...] }
- NOT(x) → { $not: [ expr(x) ] }
Базовый $match комбинирует status: 'ready' и вычисленное $expr по дереву правил.
Текущее состояние (реализовано): отбор постов по подписке на стороне API выполняется через GET /posts/for-subscription/:id по полям options.categories/categoriesExclude/categoriesAll (см. api/src/controllers/posts.js).
Модель периодичности (расписаний)¶
Поле periodicity описывает когда запускать подписку:
- trigger: boolean — если true, запускается внешним событием; если false, по расписанию.
- measure: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'by_weekdays' — базовая размерность.
- amount: number — шаг (каждые N минут/часов/дней и т.п.).
- time: string — локальное время в формате HH:mm (для дневных/недельных/месячных режимов).
- weekdays: string[] — дни недели ['mon','tue','wed','thu','fri','sat','sun'] для by_weekdays.
- timezone: string — IANA TZ для корректной локализации времени и переходов.
- datetime_start?: ISO / datetime_end?: ISO — окна активности.
Дополнительные опции (этап «Улучшения/UX»):
- «Тихие часы» (диапазоны блокировки отправок).
- Поведение при ошибках: ретраи с backoff/дефер на следующий слот.
- Глобальные/пер‑подписочные лимиты: maxPerRun, perDayCap, приоритеты.
Алгоритм calculateNextFireDateTime рассчитывает next_fire на основании текущего now, last_fire и настроек расписания с учётом TZ/границ/тихих часов.
Основные потоки и взаимодействия¶
1) Демон/воркер расписаний:
- Периодически сканирует активные Subscribe по next_fire <= now.
- Для каждой подписки вызывает API получения контента и выполняет отправку.
- По итогам фиксирует PostDelivery и SubscribeLog, пересчитывает next_fire.
2) Отбор контента для подписки:
- GET /api/v{major}/posts/for-subscription/:id?count=N возвращает кандидаты:
- $match по статусу и $expr из ContentRule;
- исключение уже отправленных (PostDelivery по subscriptionId) — план;
- сортировка (обычно по свежести createdAt: -1, возможен простой скоринг);
- лимит по countPerTick/count.
3) Отправка ботом:
- Воркеры бота берут список постов, отправляют получателю peer (Telegram и далее другие провайдеры).
- При успехе пишут PostDelivery(subscriptionId, postId, sentAt); при ошибках — логируют с деталями для ретраев.
API контракты (текущее + MVP)¶
GET /api/v{major}/subscribes/list— список (page/count_on_page), серверная пагинация.GET /api/v{major}/subscribes/search— поиск с фильтрами (bot/provider/enabled/peer.*/mode).GET /api/v{major}/subscribes/ontime— подписки, актуальные к рассылке для бота (учётnext_fire, интервала, опций).GET /api/v{major}/subscribes/get_content— текст/контент подписки по типу (используется ботом для отправки/превью).POST /api/v{major}/subscribes— создание подписки.GET /api/v{major}/subscribes/:subscribe_id— детали подписки.PUT /api/v{major}/subscribes/:subscribe_id— обновление подписки (синхронизируетperiodicity/periodicity_v2).DELETE /api/v{major}/subscribes/:subscribe_id— удаление подписки.GET /api/v{major}/subscribes/logs/stat— статистика логов за период.GET /api/v{major}/subscribes/logs/by_uuid/:uuid— детали лога по UUID.POST /api/v{major}/subscribes/logs— создать лог.PUT /api/v{major}/subscribes/logs/:log_id— обновить лог.GET /api/v{major}/posts/for-subscription/:id— подбор постов по include/exclude/all изSubscribe.options.GET /api/v{major}/posts/for-chat/:chat_id— универсальный подбор по категориям (параметры в query).- (Доп.)
GET /api/v{major}/bot/subscriptions/stat— сводная статистика для дашборда.
UI/UX админки (страница «Подписки»)¶
Роут /subscribes включает:
- Таблица (lazy, server‑side):
- Колонки: Получатель (иконка провайдера + id), Тип, Активна, Периодичность (кратко), Следующий запуск, Последний запуск, Обновлено.
- Фильтры: provider/type/enabled, текстовый поиск по peer.id/alias, даты по обновлению/следующему запуску.
- Действия: Редактировать · Дублировать · Вкл/Выкл · Логи · Удалить; массовые — Вкл/Выкл · Удалить.
-
Форма редактирования (Drawer/SidePanel) с вкладками: 1) Получатель — провайдер (readonly после создания), тип/ID, проверка существования,
enabled. 2) Тип/Контент — тип подписки (options.type), include/exclude категорий (MultiSelect), опциональные фильтры/теги, лимиты. 3) Периодичность —trigger,measure,amount,time,weekdays[],datetime_start/end,timezone; виджет превью ближайших запусков. 4) Расширенные — «тихие часы», ретраи/дефер, fallback формата, редакторoptions(JSON) для power‑users. 5) Привязка получателя — сервис‑специфичные проверки (TG: QR/ссылка для /start; VK: токен/чат). -
Логи подписки — быстрый просмотр
SubscribeLog: дата, статус/ошибка, количество отправок, фильтры и пагинация.
UI замечания (из UI_TODO и текущего стека PrimeVue): - Единый стиль заголовков/диалогов (PrimeBlocks), доступность раздела только для авторизованных. - Поведенческие улучшения: инлайн enable/disable, индикатор ближайшего запуска, консистентная тёмная тема.
Схемы и индексы (Mongo)¶
Post: индексы поstatus,source.category,createdAt(композитный{ status: 1, "source.category": 1, createdAt: -1 }).PostDelivery: уникальный композит{ subscriptionId: 1, postId: 1 }, индексы по внешним ключам.ContentRule: индексisPresetдля списка «Сетов».Subscribe: индексы поenabled,next_fire,peer.id,updatedAt(см. модель и фактические индексы).Subscription(новая модель для валидации/next_fire/summary): индексы поpeer.*,next_fire,enabled.
Синхронизация periodicity полей:
- API синхронизирует periodicity (legacy) и periodicity_v2 через controllers/utils/periodicity.{legacyToV2,v2ToLegacy,syncPeriodicityFields}.
Валидации и безопасность¶
- Доступ к разделу «Подписки» и API — только авторизованным, аналогично «Загрузчику» (JWT, проверка на фронте и в API).
- Серверная валидация расписаний (корректность TZ, диапазонов, measure/amount) и ссылочной целостности (
contentRule, существованиеpeer). - Защита от повторов (идемпотентность через
PostDelivery), лимиты на подписку/канал.
Этапы реализации (сводно)¶
1) UI‑скелет /subscribes (роут, пустая таблица, Drawer).
2) Таблица: серверная пагинация/сортировки/фильтры + базовые действия.
3) Форма: MVP (получатель, активность, тип, базовая периодичность, include/exclude категорий).
4) Превью расписания: API POST /subscribes/preview + виджет в форме.
5) Логи: GET /subscribes/:id/logs и просмотр в UI.
6) Улучшения: «тихие часы», ретраи/дефер, расширенный options, инлайн enable/disable, индикаторы.
7) Документация: Swagger/Redoc обновление, пользовательский мини‑гайд для редакторов.
Открытые вопросы¶
- Полный перечень
options.typeи их специфичных параметров. - Нужны ли глобальные анти‑спам лимиты по чатам/провайдерам.
- Где резолвить category id → name (бэк с кэшем vs фронт рантайм).
Приложение: вспомогательные заготовки¶
Псевдокод построения $match из AST правил категорий (упрощённо):
type Node = { type: 'AND'|'OR'|'NOT'|'LEAF'; categories?: string[]; children?: Node[] };
function nodeToExpr(n: Node): any { /* см. реализацию в проектных заметках */ }
export function buildRuleMatch(root: Node) {
return { $match: { status: 'ready', $expr: nodeToExpr(root) } };
}
Контур эндпоинта подбора постов:
GET /api/v{major}/posts/for-subscription/:id?count=10
// 1) найти подписку, получить include/exclude/all из options (позже — ContentRule)
// 2) собрать pipeline: match по правилу → исключить уже отправленное → сортировка → лимит
// 3) вернуть список постов