Skip to main content
Каждое сообщение, которое Omniflow отправляет клиенту, проходит через одну функцию принятия решения. Она читает вид уведомления, настройки клиента по каждому виду, его маркетинговое согласие, его тихое окно, ограничение частоты маркетинговых сообщений и состояние доставки в Telegram — и отвечает одним из трёх исходов: отправить сейчас, подождать до названного момента или не отправлять и записать причину. Эта функция — commerce.EvaluateDelivery, и незнакомый вид она отклоняет, а не трактует по умолчанию как «отправить». Поэтому добавить уведомление, не решив, транзакционное оно или маркетинговое, невозможно: новый вид просто никогда не будет доставлен, а это видимый сбой, а не тихое разглашение. Доставка выполняется в процессе bot тремя циклами. Проход жизненного цикла — уведомления о неудачных списаниях и уведомления жизненного цикла каждого клиента — идёт раз в 15 минут и обходит всех достижимых клиентов в порядке идентификаторов, по 200 за страницу, пока последняя страница не окажется короткой, — поэтому клиент, чей идентификатор сортируется последним, получает предупреждение об истечении в том же проходе, что и первый. У получателей рассылок и объявлений свой цикл: он читает по 200 тех и других, отправляет их и, пока есть очередь, сразу читает следующую пачку, а когда очереди нет — заглядывает снова каждые 30 секунд. У ответов поддержки — свой 30-секундный цикл. Каждое сообщение бота расходует один общий бюджет — APP_TELEGRAM_DELIVERY_RATE сообщений в секунду (25 по умолчанию; потолок Telegram для одного бота — около 30). Объём одной секунды может уйти сразу, дальше темп держится. Если Telegram всё же отвечает 429, его retry_after приостанавливает все отправки, потому что это ограничение бота, а не получателя: паузу до 30 секунд бот выжидает, более длинная завершает текущий проход, а попавшие под неё сообщения откладываются до её конца, а не помечаются неудачными. 429 больше не записывается на клиента, которому случилось быть получателем.

Два класса

Уведомление бывает либо транзакционным — факт о том, что клиент купил или о чём попросил, — либо маркетинговым, и тогда оно требует явного и отзываемого согласия. Классификация — это закрытая таблица в коде, а не выбор, который делает для каждого сообщения тот, кто его написал. Четыре вида дополнительно считаются срочными и никогда не задерживаются тихими часами: payment, fulfillment, support и incident. Клиенту, который находится в середине оплаты или прямо сейчас затронут аварией, не поможет сообщение, пришедшее в восемь утра следующего дня.
news — маркетинговый вид. Пост, опубликованный в категории news, поэтому требует маркетингового согласия и расходует ограничение частоты, а посты announcement, incident и maintenance — нет. Это важно, когда оператор пишет уведомление об аварии: публикуйте его как incident или announcement, а не как news, иначе push получат только те клиенты, кто согласился на предложения.

Что клиент получает на самом деле

У подписки ровно один отсчёт до окончания. У проданной Omniflow есть право на услугу, и напоминание renewal считается от его окончания и может предложить продление; у унаследованной подписки, импортированной до появления продаж, есть только срок из Remnawave, и она получает вместо этого напоминание expiry. Оба сразу не отправляются никогда: они считают одни и те же дни двумя способами и раньше приходили двумя сообщениями об одном событии — до восьми за последнюю неделю. Оповещения привязаны к конкретной подписке, поэтому активная подписка не может заглушить уведомление об окончании тихой. У каждого свой ключ дедупликации — уведомление об окончании ключуется по дате окончания и числу дней, предупреждение о трафике — по порогу и лимиту, — и именно это не даёт напоминанию повторяться на каждом проходе целую неделю. Два пути намеренно обходят механику политик:
  • Ответы поддержки отправляются собственным 30-секундным циклом без проверки согласия, тихих часов и лимита частоты — ответ клиент сам попросил. Строку доставки вида support они оставляют, поэтому история ниже показывает каждый ответ как доставленный, ожидающий или придержанный с причиной (bot_blocked, no_telegram); само сообщение в любом случае можно прочитать на /account/support.
  • Уведомления о неудачном автосписании (dunning) отправляются независимо от настроек и тихих часов. Клиенту, который вот-вот потеряет доступ из-за отклонённой карты, не поможет придержать сообщение до утра, и в модели настроек для него нет отключения.
Клиент без идентичности в Telegram — тот, кто входил только по ссылке из бота или через OIDC-провайдера — никогда не попадает в набор получателей ни для чего из перечисленного. Ответы оператора и объявления он видит в веб-панели, когда открывает её, и никуда ему ничего не отправляется. Отправителей электронной почты и SMS в Omniflow нет.

Как задать настройки

Обе поверхности опираются на одну и ту же строку bot_preferences, поэтому выбор, сделанный в одной, немедленно действует и в другой. Обе пишут в одни и те же четыре переключателя, одно и то же поле языка и одно и то же тихое окно.

В браузере

Откройте /account/preferences — переход из Аккаунт → Уведомления в центре на /account/profile.
1

Выберите, что мы отправляем

Четыре переключателя, каждый включён по умолчанию: Окончание подписки, Предупреждения о трафике, Продления и платежи и Объявления. Переключение одного отправляет PATCH только с этим полем.
2

Выберите язык сообщений

Как в аккаунте, Русский или Английский. Это язык, на котором вам пишет бот, а не язык панели — собственный язык панели задаётся на /account/profile.
3

Задайте тихие часы

Главный переключатель включает окно по умолчанию 23:00–08:00; два списка затем сдвигают час начала и час окончания. Выключение переключателя отправляет {"startHour": 0, "endHour": 0}, а не опускает поле, потому что опустить поле означает «оставить окно как есть», то есть противоположное тому, о чём попросили.
4

Решите насчёт предложений

Переключатель Предложения и акции — это маркетинговое согласие. Под ним указаны дата, поверхность и версия условий того решения, которое действует сейчас.
Экран также показывает действующую блокировку отправки (только для чтения) и по одной строке на каждый контактный канал с его флагами подтверждения, служебных сообщений и рекламы. Сами адреса этот маршрут никогда не возвращает — API вообще не выбирает шифротекст, а это более сильная гарантия, чем помнить про необходимость убрать его перед записью ответа. Адресами управляют на /account/privacy. Каждое поле обновления на сервере — указатель, и именно это делает запрос настоящим PATCH: отсутствующее поле остаётся нетронутым, поэтому экран, рисующий один переключатель, не может молча переписать те, которых он никогда не показывал. Ответ — это полностью перечитанный документ, поэтому панель целиком заменяет свой кэш вместо ревалидации.

В Telegram

Отправьте /settings. На экране есть строка языка, те же четыре переключателя, маркетинговый переключатель и пункт тихие часы, открывающий подменю из трёх пресетов: 22:00–08:00, 23:00–09:00 и «выключено». Пресеты — это явный список, а не поле для произвольного ввода времени, поэтому здесь нет разбора времени, в котором можно ошибиться, а данные обратного вызова остаются закрытым множеством. Когда настроено ограничение частоты маркетинговых сообщений, экран настроек в Telegram его сообщает. Веб-экран — нет.

Какой переключатель управляет какими сообщениями

Последняя строка — суть всей модели. Эти виды являются следствием собственных действий клиента, и модель настроек, позволяющая их отключить, позволила бы кому-то тихо потерять доступ. У клиента, который ни разу не открывал экран настроек, строки настроек нет вовсе; чтение падает на значения столбцов по умолчанию — все четыре переключателя включены, маркетинг выключен, язык auto.

Тихие часы

Тихое окно — это окно по местному времени, в течение которого несрочные сообщения ждут, а не отбрасываются. Окно, конец которого не позже начала, переходит через полночь, поэтому 23:00–08:00 — это одно окно, а не два. Равные часы означают отсутствие окна, и обе поверхности используют это соглашение, чтобы его снять. Когда сообщение попадает внутрь окна, строка доставки помечается как deferred с моментом окончания окна. Следующий проход заново забирает отложенную строку, чей момент уже наступил, — именно это превращает отсрочку в задержку, а не в потерю.
Окно вычисляется в users.timezone, значение по умолчанию — UTC. Его задают обе поверхности: веб-экран настроек предлагает все зоны IANA, которые знает браузер, а кнопка бота 🌍 Часовой пояс — на экране настроек и на экране тихих часов — предлагает список распространённых зон с их текущим смещением. Бот называет зону рядом с окном везде, где его показывает, а клиенту, который всё ещё на значении по умолчанию, сообщает, что тихие часы считаются по UTC. Если язык его клиента Telegram подсказывает зону — русский подсказывает Москву, украинский Киев, казахский Алматы и так далее, — выбор сначала спрашивает о ней, вопросом с кнопкой ✅ Да; сам бот зону никогда не выставляет.

Маркетинговое согласие и что делает отписка

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

Отписка

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

Блокировка отправки

Блокировка отправки — это не флаг согласия. Согласие — это настройка, которую клиент переключает; блокировка переживает случайное обратное включение, и её также может записать оператор из-за возвратов или жалобы. Экран настроек сообщает о действующей блокировке с её причиной и датой вступления в силу и не предлагает элемента, чтобы её снять, — удержание, поставленное потому, что сообщения раз за разом не доходили, этот экран не вправе отменять. Обратное включение маркетинга удаляет только блокировку с причиной customer_request, а отписка никогда не перезаписывает операторскую: если строка с причиной bounced, complaint или operator уже есть, отписка клиента оставляет её как есть, так что два клика — отписаться, затем снова включить — не превратят вывод о возвратах в снятое удержание. Проход доставки читает этот список. Маркетинговое сообщение — рассылка или пост категории news — заблокированному клиенту придерживается с причиной suppressed, что бы ни говорил флаг согласия, и история показывает его под этой причиной, а не как no_consent. Транзакционные сообщения не затрагиваются: заблокированный клиент по-прежнему узнаёт, что его подписка заканчивается в четверг. Операторы видят список в /admin/marketing → Блокировки отправки и могут снять запись там; добавление — это вызов API, а не экран. См. рассылки.

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

Обе читаются процессом bot. Счёт берётся по строкам доставки с классом marketing и статусом sent внутри окна, поэтому заблокированное или отложенное сообщение лимит не расходует. Поскольку посты категории news являются маркетинговыми, объявление, опубликованное в этой категории, конкурирует за те же три места, что и рассылка.

Решение, по порядку

EvaluateDelivery выполняет эти проверки в такой последовательности и останавливается на первой, которая даёт ответ:
  1. Состояние доставки в Telegram — blocked → отказ с причиной bot_blocked; deactivated → отказ с причиной user_deactivated; not_started → отказ с причиной not_started.
  2. Настройка по данному виду выключена → отказ с причиной kind_disabled.
  3. Сообщение маркетинговое, а согласия нет → отказ с причиной no_marketing_consent.
  4. Сообщение маркетинговое, а лимит израсходован → отказ с причиной frequency_cap.
  5. Ещё действует flood wait от Telegram → отложить до этого момента.
  6. Вид несрочный, и момент попадает внутрь тихих часов → отложить до конца окна.
  7. Иначе — отправить.
Затем бот применяет одно вето, о котором сама функция не знает: маркетинговое сообщение, вышедшее из последовательности как «отправить» или «отложить», отклоняется с причиной suppressed, если клиент есть в communication_suppressions. Оно стоит вне функции, потому что блокировка — это запись о клиенте, а не его настройка, и у поверхностей, разделяющих функцию, нет списка, с которым сверяться. Каждая попытка оставляет строку в notification_deliveries, уникальную по (user_id, kind, subscription_id, dedupe_key) с NULLS NOT DISTINCT, чей статус — один из pending, deferred, sent, failed или suppressed и которая несёт класс и причину отказа. Строка failed повторяется, пока её счётчик неудач меньше трёх; строка deferred повторяется, когда её момент наступил; строка suppressed окончательна. Собственные сбои Telegram классифицируются в устойчивое состояние, чтобы заблокированный или удалённый аккаунт перестал расходовать повторы: bot_blocked помечает получателя как blocked, а user_deactivated или chat_not_found — как deactivated, и ни то ни другое не повторяется, тогда как flood wait, таймаут или авария Telegram помечают его как failing, и это повторяется. Ответ Telegram «bot can’t initiate conversation with a user» — так бывает с тем, кто ни разу не нажимал Start, например с клиентом, вошедшим на сайт через виджет Telegram, — помечает его как not_started: это так же окончательно, но это не блокировка. blocked, deactivated и not_started исключают клиента из каждого прохода уведомлений, поэтому успешная отправка никогда не может их снять. Вместо этого бот снимает их, как только слышит этот аккаунт: любое сообщение, нажатие кнопки или оплата из личного чата клиента возвращает состояние в active. Собственное сообщение Telegram о том, что бота в этом чате заблокировали или перезапустили (my_chat_member), тоже учитывается: блокировка записывается как blocked ещё до того, как об неё споткнулась хоть одна отправка, а перезапуск её снимает.

Лента объявлений

Push можно пропустить. Лента — это долговременное место, куда клиент может вернуться, и на обеих поверхностях это одна и та же запись: прочитанный в Telegram пост перестаёт быть непрочитанным в вебе. В браузере — /account/news, переход из /account/support → Новости. Посты перечислены от новых к старым, по 20 за раз за элементом Показать более старые объявления, с числом непрочитанных в заголовке. У каждого поста есть значок категории — announcement в информационном тоне, incident в тоне опасности, maintenance в предупреждающем тоне, news нейтрально, — а пост с классом marketing получает второй значок Реклама, потому что клиент, согласившийся на предложения, вправе видеть, что здесь служебное уведомление, а что — продающее сообщение, не выводя это из формулировок. Тела постов отображаются как обычный текст; ничто в посте не интерпретируется как разметка. Прочтение — явное действие: нажмите Отметить прочитанным. Панель не выводит его из положения прокрутки, потому что общее состояние прочтения — это утверждение, которое стоит делать только тогда, когда клиент действительно так сказал. В Telegram — пункт меню Новости со значком непрочитанного, затем список и просмотр поста.

Какие посты видны

Пост виден, когда его статус — published, момент публикации задан и уже наступил, а сам пост не истёк. Проверка статуса несёт нагрузку: снятие с публикации намеренно сохраняет отметку публикации, чтобы повторная публикация не переписывала момент, когда клиенты впервые увидели пост, — и одно только окно по отметкам времени поэтому продолжало бы показывать пост, который оператор снял. Язык разрешается на сервере: явный запрос выигрывает, затем настройка бота (если она не auto), затем язык аккаунта, затем английский. Пост, у которого нет перевода на разрешённый язык, отсутствует, а не показывается на другом языке, потому что ленту, которая переключает язык посреди списка, читать труднее, чем более короткую. Разрешённый язык возвращается вместе с ответом, и панель показывает уведомление, когда он отличается от того, на котором отрисована страница. Веб-панель намеренно не отправляет собственный язык, чтобы один и тот же клиент не читал одно и то же объявление на двух языках в зависимости от того, какую поверхность он открыл.
Операторского экрана для написания объявлений нет. Единственный переход состояния, который выполняет хоть какой-то код, — это проход воркера scheduled → published, который выполняется примерно раз в минуту и продвигает до 50 постов, чей запланированный момент наступил. Создание поста, написание его переводов, снятие с публикации и архивирование делаются прямой записью в news_posts и news_post_localizations.
Push, сопровождающий пост, отправляется один раз на клиента на пост — клиентам с получателем в Telegram и переводом на их разрешённый язык — и только для постов, опубликованных в течение последних семи дней: пост, который проход доставки впервые видит, когда он старше, не анонсируется никогда.

Уведомления браузера

/account/preferences → Уведомления браузера → Включить уведомления браузера. Запрос разрешения от браузера следует за этим нажатием, а не за загрузкой страницы, потому что браузер даёт сайту один безропотный шанс спросить, а рефлекторное «Заблокировать» действует навсегда и со страницы не отменяется. Это API Notification, работающий на опросах, которые панель и так выполняет, — и это явно не web push. Здесь нет service worker, нет ключа VAPID и никуда не отправляется подписка, потому что у Omniflow нет серверной отправки push, чтобы её доставлять, а подписка, которую сервер никогда не использует, — это запрос разрешения, потраченный впустую. Когда вкладка закрыта, до клиента добирается Telegram. Что это означает на практике:
  • Согласие живёт в localStorage под ключом omniflow.account.browser-notifications, поэтому это настройка одного браузера, а не аккаунта, и она исчезает вместе с данными сайта. Флаг на уровне аккаунта заявлял бы настройку, которую одно устройство соблюсти не может.
  • Обе половины должны совпадать. Разрешение, отозванное в собственных настройках браузера, очищает сохранённое согласие при следующем чтении.
  • Пока клиент не дал согласие, вообще ничего не запрашивается; после этого список обращений и список новостей опрашиваются каждые 60 секунд.
  • Ничего не показывается, пока вкладка видима и в фокусе. Функция существует для вкладки, оставленной в фоне.
  • Первый опрос после согласия только устанавливает базовую точку, поэтому открытие панели с тремя непрочитанными ответами ничего не вызовет.
  • Повторы схлопываются по обращению и по посту через тег уведомления.
  • Отклонённое разрешение никогда не запрашивается повторно; карточка сообщает, что его нужно изменить в настройках браузера.
  • Chrome на Android бросает исключение из конструктора Notification, потому что уведомления там требуют service worker. Это перехватывается и сообщается как «не поддерживается», с пояснением на карточке, а не становится тихой пустышкой.
  • Обнаружение ответа сравнивает серверный счётчик непрочитанного, который вычисляется из общей записи сообщений, — поэтому ответ, уже прочитанный в Telegram, приходит сюда без повода что-либо сообщать.

Что было отправлено на самом деле

Всё, что выше, — это настройки. Настройка говорит, что должно приходить, и поэтому «я не получил предупреждение об истечении» остаётся утверждением, которое никто в разговоре не может проверить: у клиента есть только отсутствие сообщения, а отсутствие выглядит одинаково — сломан ли бот, удержало ли сообщение тихое окно или отправлять было нечего. Запись существовала с первого релиза. notification_deliveries всегда несла тип, класс, ушло ли сообщение, когда, сколько было попыток и — если не ушло — код с причиной. Её никто не читал. Теперь её читают обе стороны.

Собственная история клиента

Под элементами настроек, на том же экране, сначала новые. Текстов сообщений там нет: запись говорит, что уведомление такого-то типа состоялось, а не что в нём было, и подстановка шаблона под сегодняшние данные показала бы человеку то, что ему никогда не отправляли. Что означает каждый исход: Причины удержания указывают на элемент управления на этой же странице: quiet_hours — на тихое окно, no_consent — на переключатель, frequency_cap — на настройку маркетинга. В этом и смысл соседства: «ничего не пришло» превращается в то, что клиент может изменить сам, не создавая обращение.

Что видит оператор

/admin/customers/{id}, вкладка Уведомления. Для чтения нужно customers.read: история доставок — часть записи клиента, и тот, кто отвечает на «вы отправляли?», уже смотрит на этот аккаунт. Те же строки, со сводкой по типам сверху и фильтром по исходу. Полезнее всего фильтр Удержано: именно он разворачивает разговор с поддержкой. Статус «удержано» показан нейтральным цветом, а не как предупреждение, — это установка делает то, что ей велели, и окраска его как сбоя отправила бы операторов искать проблему в работающей функции.

Отправка теста

Оператор может отправить одно сообщение одному клиенту, чтобы убедиться, что доставка работает прямо сейчас. Нужно право support.write: в клиенте ничего не меняется, уходит сообщение, и это нужно тем, кто ведёт разговор, из которого вопрос возник. Тест идёт через обычную очередь отправки, и в этом вся его ценность. У процесса панели нет соединения с Telegram — оно есть у рассыльщика. Тест, который пошёл бы своим путём, доказал бы, что работает путь теста, о чём никто не спрашивал. Этот забирается тем же воркером, отправляется тем же вызовом, классифицируется теми же кодами ошибок и записывается в ту же таблицу — поэтому, когда настоящие уведомления падают, тест падает так же и называет причину. Три следствия, которые стоит назвать прямо:
  • Он поставлен в очередь, а не отправлен. API отвечает 202, и панель так и говорит. Сообщение уйдёт в следующем проходе рассыльщика, а результат появится в истории под кнопкой. Сообщать об отправке, которой ещё не было, как о состоявшейся — ровно тот сбой, ради которого эта функция и существует.
  • Тихие часы, лимит частоты и переключатели по типам не применяются. Тест, отложенный до девяти утра, ничего не отвечает в тот момент, когда спрашивают. Состояние доставки применяется по-прежнему: клиента, заблокировавшего бота, не достать желанием, и это как раз тот вывод, который оператору и нужен.
  • У него собственный тип. Тест никогда не прочитается как настоящее уведомление и никогда не потратит бюджет частоты рекламных сообщений.
Дедупликация по оператору и по минуте: нетерпеливый двойной клик — это одно сообщение, а два оператора, проверяющие одного клиента, — два, потому что это два вопроса. Панель сообщает, что именно произошло. Клиент получает сообщение, где прямо сказано, что это тест, отправленный оператором, и что от него ничего не требуется, с отметкой времени. Необъяснённое сообщение от VPN-провайдера хуже, чем отсутствие сообщения. Каждая отправка теста попадает в журнал аудита как customer.notification.tested.

Справочник

Отказы — это problem-документы RFC 9457. locale должен нормализоваться в auto, ru или en — значение сначала приводится к нижнему регистру, поэтому отказ на EN был бы правилом о регистре, а не о языке. Тихие часы должны быть целыми часами от 0 до 23. Несуществующая запись клиента отвечает 404. Операторские маршруты, все под /v1/panel:

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

Клиентская веб-панель

Остальное, что клиент может сделать из браузера.

Telegram-бот

/settings, меню новостей и все остальные команды.

Персональные данные

Контактные каналы, след согласий, экспорт и удаление.

Тексты сообщений

Со стороны оператора: изменение этих уведомлений, с предпросмотром и тестовой отправкой.

Рассылки

Сторона оператора: сегменты, шаблоны, отправки и блокировки.