Skip to main content
Всё, что Omniflow отправляет клиенту, проходит через два механизма, описанных здесь: входящие с новостями, где долговременно живут объявления, и политика доставки — единственное место, где решается, можно ли вообще писать клиенту. Оба существуют по одной причине. Клиенту нужно сообщить, что его подписка заканчивается в четверг, независимо от того, хочет ли он маркетинг, а оператору нужна запись о каждом неотправленном сообщении и о причине. Сведение этих двух вещей в один переключатель «уведомления» сделало бы одну из них невозможной. Эта страница — взгляд оператора и системы. О том, что видит и чем управляет клиент, см. Уведомления и согласие.

Новости и объявления

Категории и во что каждая обходится

Пост несёт category и class. Категорий четыре, и именно категория считается для машинерии доставки видом уведомления: Все четыре дополнительно управляются единственным переключателем настройки новостей у клиента, поэтому клиент, выключивший новости, перестаёт получать и объявления, и уведомления об авариях.
news_posts.class — отдельная колонка, доводимая до полезной нагрузки клиентских входящих, где панель использует её для окраски бейджа. На доставку она не влияет — это решает категория.

Жизненный цикл и что на самом деле способно провести пост по нему

news_posts.status — одно из draft, scheduled, published, unpublished или archived, а рядом стоят scheduled_for, published_at, unpublished_at и archived_at. Одна отметка времени публикации не может выразить «написано, но не готово», «запланировано на вторник» или «снято, потому что было неверным» — а последнее нужно оператору срочнее всего, поэтому снятие с публикации существует как состояние, а не как удаление. published_at выставляется через COALESCE(published_at, now()), поэтому повторная публикация не переписывает момент, когда клиенты впервые увидели пост. CHECK требует scheduled_for всегда, когда статус — scheduled.
Операторской поверхности для новостей нет. Единственный переход состояния, который делает хоть какой-то код, — это scheduled → published в воркере: проход campaign_expansion выполняется раз в минуту, берёт до 50 постов, у которых scheduled_for уже прошёл, и публикует их. Создать пост, написать его русскую и английскую локализации, запланировать его, снять с публикации или заархивировать можно только записью напрямую в news_posts и news_post_localizations.Слаги соответствуют ^[a-z0-9][a-z0-9_-]{2,63}$, заголовки ограничены 200 символами, тела — 3500, локали — только ru или en.

Что делает пост видимым

Пост виден клиенту, когда выполняется всё перечисленное:
Проверка статуса важна сама по себе. Снятие с публикации намеренно оставляет published_at заполненным — это момент, когда клиенты впервые увидели пост, — поэтому одно только окно по отметкам времени продолжало бы показывать пост, который оператор снял. И веб-панель, и бот применяют одинаковый предикат, поэтому отзыв поста убирает его сразу с обеих поверхностей.

Разрешение локали

Пост существует по локалям, и пост, у которого нет перевода на разрешённую локаль клиента, отсутствует, а не показывается на другом языке. Разрешение идёт по порядку: явный параметр запроса locale, если он называет ru или en, затем настройка клиента в боте, если она не auto, затем локаль аккаунта, затем en. Разрешённая локаль возвращается в ответе, поэтому панель может сказать, что получила не тот язык, который просила, а не делать вид.

Входящие

Элементы упорядочены сначала новые по (published_at, id) на обеих поверхностях, поэтому пачка, опубликованная в один и тот же момент, читается везде в одном и том же порядке. unreadCount считает каждый видимый пост, имеющий локализацию в разрешённой локали и не имеющий строки о прочтении, и не ограничен страницей. Отметка поста прочитанным сначала проверяет видимость и отвечает 404 для невидимого; вставка идёт с ON CONFLICT DO NOTHING, поэтому прочитать дважды — не ошибка. Клиенты попадают сюда по адресу /account/news в веб-панели или через пункт меню News в боте, и на каждом висит бейдж непрочитанного.

Push

Входящие работают по запросу. Push — это то, благодаря чему уведомление об аварии доходит. Нотификатор бота ищет посты, опубликованные за последние семь дней, для активных клиентов с получателем в Telegram, у которых есть локализация в разрешённой локали клиента и нет строки доставки для этого поста, которая ещё ждёт или уже окончательна. Он отправляет по одному сообщению на клиента на пост, по 200 за раз, и читает следующую пачку сразу после отправки предыдущей — в темпе общего для бота APP_TELEGRAM_DELIVERY_RATE. Клиент, чей чат заведомо недоступен — blocked, deactivated или not_started, — вообще не читается, а клиент, ставший недоступным между чтением и отправкой, получает строку доставки suppressed с причиной unreachable. В обоих случаях очередь идёт дальше; раньше такие клиенты пропускались без записи, стояли в голове каждой пачки все семь дней, и двести таких останавливали объявления для всех. Доставка, отложенная до конца тихих часов или провалившаяся с оставшимися повторами, читается снова, как только может сдвинуться, поэтому отсрочка — это задержка, а не потеря. Пост, которому к моменту, когда воркер впервые его увидел, уже больше семи дней, не анонсируется никогда. Публикация старого поста задним числом наполнит входящие и не отправит ничего.

Как классифицируется каждое уведомление

Каждое уведомление клиенту проходит через одну функцию принятия решения. Неклассифицированный вид отклоняется, а не доставляется, поэтому добавить уведомление, не решив, какого оно класса, невозможно. Срочные виды никогда не откладываются тихими часами: payment, fulfillment, support и incident. Всё остальное ждёт окончания окна.

Какая настройка управляет каким видом

Четыре вида, от которых нельзя отказаться, несут факты, нужные клиенту независимо от того, хочет ли он слышать о бизнесе. Модель настроек, способная заглушить сообщение о сбое платежа, была бы способом потерять доступ без предупреждения.

Порядок принятия решения

  1. Клиент заблокировал бота — отказ, bot_blocked. Его аккаунт деактивирован — отказ, user_deactivated. Он ни разу не запускал бота — отказ, not_started. Ничто из этого не повторяется, и все три состояния снимаются, когда клиент в следующий раз пишет боту или нажимает в нём кнопку.
  2. Настройка для этого вида выключена — отказ, kind_disabled.
  3. Класс marketing без согласия — отказ, no_marketing_consent.
  4. Класс marketing и квота частоты уже превышена — отказ, frequency_cap.
  5. Retry-after в будущем — отложить до этого момента.
  6. Не срочное и внутри тихих часов — отложить до конца окна.
  7. Иначе разрешить.

Тихие часы

Тихие часы — это окно в целых часах по местному времени, вычисляемое по users.timezone. Окно переходит через полночь, когда час окончания не больше часа начала, а start == end означает, что окна нет вовсе, — именно так на обеих поверхностях выражается выключение тихих часов.
Часовой пояс, в котором вычисляется окно, — это пояс аккаунта, а экран настроек клиента его не показывает и не меняет. Клиент, задающий 23:00–08:00 в браузере, задаёт их в том часовом поясе, который уже записан у его аккаунта.

Ограничение частоты

Обе читаются процессом бота. Счёт — это число строк доставки с классом marketing и статусом sent внутри окна, поэтому подавленное или отложенное маркетинговое сообщение не расходует квоту клиента. Экран /settings в боте показывает клиенту ограничение, когда оно настроено.

Долговременная запись

notification_deliveries — это запись каждого решения, уникальная по (user_id, kind, subscription_id, dedupe_key) с NULLS NOT DISTINCT. Строка берётся в работу повторно только тогда, когда она failed с числом сбоев меньше трёх или deferred с истёкшей отсрочкой. Именно это превращает отсрочку в задержку, а не в потерю сообщения. Ключ включает подписку, поэтому уведомления жизненного цикла идут по каждой подписке отдельно и активная подписка не может подавить уведомление о сроке окончания у тихой. Сбои Telegram классифицируются в долговременное состояние доставки на аккаунте: bot_blocked превращается в blocked без повторов, user_deactivated и chat_not_found — в deactivated без повторов, «bot can’t initiate conversation» — в not_started без повторов, а флуд-ожидания, таймауты и недоступность Telegram — в failing, который повторяем. Любое обновление из личного чата клиента и сообщение Telegram my_chat_member о перезапуске бота снимают три окончательных состояния; блокировка из my_chat_member записывается как blocked.

Три намеренных исключения

  • Уведомления о просроченных платежах полностью обходят политику. Клиенту, который вот-вот потеряет доступ из-за отклонённой карты, не поможет придерживание сообщения до утра, и в модели настроек отказаться от него тоже нельзя. Каждое уведомление всё же оставляет строку доставки payment с ключом dunning:<attempt>, и она его ограничивает: неудачная отправка повторяется в двух следующих проходах и прекращается, отказ, который Telegram считает окончательным, прекращает её сразу, клиенту с заведомо недоступным чатом отправка не делается, а уведомление старше семи дней не отправляется.
  • Ответы поддержки тоже её обходят. Проход доставки ответов отправляет напрямую и не пишет строку доставки; дедупликация берётся из собственного delivered_at сообщения. Как следствие, у ответа поддержки нет записи о подавлении и нет учёта повторов. Он в любом случае был бы классифицирован как срочный.
  • Клиент без Telegram-идентичности не получает push вообще. Получатели берутся из активных Telegram-идентичностей и Telegram-идентификаторов Remnawave. Клиент, вошедший только по магической ссылке или через OIDC, видит ответы и объявления в веб-панели, и никуда ничего не отправляется.

Согласие дописывается, а не редактируется

Каждое маркетинговое решение дописывает строку consent_records, несущую цель, факт выдачи согласия, версию политики, поверхность-источник и идентификатор запроса. Текущим ответом считается самая новая строка. История, которую можно переписать, ничего не доказывает. Обновление настройки дописывает строку только тогда, когда значение действительно меняется относительно последней записи. Отписка дописывает безусловно, потому что «он и так уже отписался» — не причина, чтобы его отписка не оставила следа. Колонка marketing_enabled в строке настроек — это запасной вариант для установок, появившихся раньше истории согласий. Там, где они расходятся, побеждает запись. Именно это делает возможной строку с доказательством, показываемую клиенту: «вы дали согласие» — не слишком полезное сообщение тому, кто этого не помнит, а «вы дали согласие в такую-то дату, с такой-то поверхности, под такой-то версией политики» — полезное.

Что делает отписка

Одна кнопка на /account/preferences выключает маркетинг и настройку новостей, снимает маркетинговый флаг со всех живых контактных каналов, дописывает отзыв согласия и пишет строку communication_suppressions с причиной customer_request. Она намеренно не трогает уведомления об окончании срока, о трафике и о продлении. Они несут факты, нужные клиенту независимо от того, хочет ли он слышать о бизнесе, и отписка, заглушающая их, превратила бы маркетинговую настройку в способ потерять доступ без предупреждения. Обратное включение маркетинга позже удаляет только подавление customer_request — о том, почему записанное оператором подавление это переживает, см. список подавления.

Вложения в поддержке и сроки хранения

Вложение в обращении хранится одним из двух способов, и разница важна с операционной точки зрения. Вложение из Telegram хранит только идентификатор файла в Telegram. Хранителем выступает Telegram, а Omniflow не держит байтов. У загрузки через веб такого хранителя нет, поэтому байты пишутся на диск под ключом, адресуемым по содержимому. Какой из двух видов перед вами — это колонка: origin равен telegram или local, а ссылка лежит соответственно в telegram_file_id или в storage_key. Таблица сама следит за этой парой, поэтому строка не может объявить одно происхождение и нести ссылку другого. Всё, что отправляет вложение обратно в Telegram, пропускает строку с local — дайджест содержимого ничего не значит для Telegram, — а маршрут скачивания отклоняет строку с telegram, потому что этих байтов у него нет.
API и воркер должны указывать на один и тот же каталог. Байты пишет API, а удаляет их обход воркера. Если они расходятся, обход удаляет строки, а файлы остаются, и диск растёт без ограничения.
Сам срок хранения вложений — это значение колонки по умолчанию, 90 дней, и он не настраивается. Ежечасный обход удаляет каждую истёкшую строку, а затем удаляет только те веб-файлы, на которые не ссылается ни одна уцелевшая строка: два клиента, загрузившие одинаковые байты, делят один файл, адресуемый по содержимому, поэтому удаление по строке удалило бы чужое вложение. Файлы удаляются только после фиксации транзакции, а файл, который не удалось удалить, записывается в лог и пропускается, а не роняет весь обход.
Истёкшее вложение удаляет только обход воркера. Раньше бот тоже вычищал строки в конце каждого прохода уведомлений, и этот путь не освобождал файлы — поэтому всякий раз, когда он срабатывал первым, строки уже не было, обходу нечего было освобождать, и байты оставались на диске навсегда. Он убран: удаляющий один, и это тот же, кто освобождает диск.

Смежные страницы

Сегменты аудитории и кампании

Отправка сообщения выбранной аудитории и список подавления.

Уведомления и согласие

Та же машинерия со стороны клиента.

Служба поддержки

Куда приходят вложения и как доставляются ответы.

Конфигурация

Каждая переменная окружения, названная на этой странице.