Уведомления — Web Push, mute, mentions
Перед началом: окно диалога и группы (Модуль 9). Инструменты: Web Push (VAPID), service worker, Go. Трудоёмкость: ~6 ч.
🔥 Инцидент: «Пять пушей в минуту и веер отписок»
Группа на 300 человек — рабочая, живая. Уведомления настроили по-честному: на каждое сообщение — push всем, кто офлайн. В первый же активный час пользователи получили десятки пушей, телефон превратился в сирену — дальше посыпались отписки от уведомлений (и часть — от сервиса целиком). Второй инцидент: mute работал… наполовину. «Не беспокоить» на группу гасило пуши в вебе, но не в push — потому что правило фильтрации жило на клиенте, а пуш-воркер о нём не знал. В этом модуле: правила уведомлений на сервере, упоминания, агрегация.
📖 Теория
10.1 Кого и когда будить: правила
Правила — данные продукта, фиксируем явно (и держим на сервере):
| Событие | direct | группа |
|---|---|---|
| Сообщение, получатель офлайн | 🔔 push | 🔕 тишина |
Упоминание @вы | 🔔 push | 🔔 push |
| Ответ на ваше сообщение | 🔔 push | 🔔 push |
| Диалог в mute | 🔕 | 🔕 (кроме упоминания) |
| Получатель онлайн (есть соединения) | 🔕 | 🔕 |
Почему на сервере: клиент может быть закрыт (в этом весь смысл пуша) — значит, правила обязан применять тот, кто отправляет push. Клиентская фильтрация «на будущее» невозможна в принципе.
10.2 Кто офлайн
У нас уже есть всё: presence-ключ (Модуль 7) и таблица соединений пользователя (Модуль 6). «Офлайн» = нет активных соединений; Redis присутствие — подсказка, соединения — факт. Пуш шлём, только если соединений ноль — не дублируем живое приложение.
10.3 Подписки: endpoint + ключи
Браузер выдаёт объект подписки (endpoint, keys.p256dh, keys.auth) —
храним у пользователя (одна запись на устройство):
sqlCREATE TABLE push_subscriptions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, endpoint TEXT NOT NULL UNIQUE, p256dh TEXT NOT NULL, auth TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );
Отправка — библиотекой (например, SherClockHolmes/webpush-go) с
VAPID-ключом: VAPID_PUBLIC отдаём фронтенду, VAPID_PRIVATE живёт в
env. Ответ 404/410 от endpoint — подписка протухла, удаляем строку.
10.4 Service worker и упоминания
Фронтенд: регистрируем минимальный service worker — обработчик push
(показать showNotification) и notificationclick (сфокусировать
вкладку на нужном диалоге). Всё остальное (PWA-обвязку) — вне курса.
Упоминания: в тексте @username; сервер находит пользователя по
display_name-слагу и добавляет получателя в правило §10.1. Полноценные
mentions (автокомплит, уведомления по id, Unicode-имена) — упражнение;
важно сейчас: mention — это отдельный сигнал, а не подстрока
(проверяем по границам слова и списку участников диалога).
10.5 Агрегация
Окно ~30 секунд: несколько сообщений одному офлайн-получателю → один
push («3 новых сообщения от «Команда»»). Учебная реализация простая:
буфер в памяти воркера userID → batch c flush по таймеру; в продакшене
это же делается в Redis. Анти-спам не только про нервы: у браузерных
push-сервисов есть лимиты, а у пользователя — терпение.
💻 Практика
-
Миграция
000007_push_subscriptions.{up,down}.sqlпо §10.3. -
API:
POST /api/v1/push/subscriptions(upsert по endpoint от текущего пользователя),DELETEпо endpoint (отписка). VAPID-ключи — в env;GET /api/v1/push/public-keyдля фронтенда. -
Воркер: расширьте обработку
message.newиз outbox:- определите получателей-офлайн (соединения = 0, правило §10.1);
- примените правила (direct / mention / reply / mute по
muted_until, §5.1); - отправьте push (или отложите в буфер агрегации §10.5).
-
Фронтенд:
features/enable-push: запрос разрешения,pushManager.subscribe, отправка подписки на бэкенд; кнопка «Включить уведомления» в настройках;public/sw.js— обработчикиpushиnotificationclick(переход в диалог);- in-app: счётчик непрочитанных в
document.titleи Notifications при скрытой вкладке (без SW).
-
Mute:
PATCH /conversations/{id}сmuted_until(+ кнопка в member-panel из Модуля 9). Проверьте инцидент: в muted-группе пуш не приходит, кроме упоминания. -
Тесты и сценарий:
- Боб офлайн → Алиса пишет в direct → Боб получает push; Боб онлайн → push нет, есть WS;
- группа: 10 сообщений за 30 секунд Бобу офлайн → один агрегированный push;
@bobв muted-группе → push приходит;- протухшая подписка (удалить руками из БД и эмулировать 410) — строка вычищается, ошибок наружу нет.
Чек-лист модуля: правила уведомлений применяет сервер; онлайн-пользователя не дублируем push'ем; mute гасит, mention пробивает; агрегация работает; протухшие подписки вычищаются.
🧩 Quiz
✅ Итоги модуля
- Правила «кого будить» — на сервере: direct/mention/reply/mute, онлайн не дублируем.
- Подписки устройств хранятся в БД; протухшие вычищаются по 404/410.
- Service worker показывает push и открывает нужный диалог.
- Mute и mentions — часть единой матрицы правил, а не клиентские фильтры.
- Агрегация окна гасит шквал и бережёт лимиты push-сервисов.
🔗 Мостик к Модулю 11
Функционально мессенджер полон. Два вопроса перед продакшеном: выдержит ли он нагрузку (10 000 соединений — это не «много», но и не «мало») и что произойдёт, когда процесс перезапустится, а все клиенты одновременно пойдут в sync. Следующий модуль — нагрузка, метрики, шторм реконнектов и деградация.