Уведомления в браузере и на телефоне: сервер сам решает, кому их слать, и
подписывает запрос VAPID-ключом, поэтому уведомление приходит, даже когда
клиент закрыт.
Сервер:
- миграция 00021: `push_subscriptions` (эндпоинт уникален, ключи, счётчик
неудач, время последней доставки);
- `internal/push` — VAPID-ключи (приватный PKCS#8 из конфига, публичный
выводится из него), правила уведомлений повторяют
`web/src/lib/desktopNotifications.ts` (упоминания и личные беседы, без своих
и системных сообщений), очередь доставки, TTL 12 часов, Topic по комнате;
- 404/410 от push-сервиса удаляют подписку сразу, 5 неудач подряд — тоже,
иначе копились бы мёртвые эндпоинты; retention чистит «молчащие» подписки;
- `internal/httpx/safeurl.go` — общий запрет внутренних адресов с проверкой
адреса в момент подключения (DNS rebinding): эндпоинт подписки приходит от
клиента, и без проверки сервер сам себе организует SSRF;
- `internal/gateway/presence.go` — `IsUserOnline`: если получатель в клиенте,
уведомление покажет клиент, дублировать на телефон не нужно;
- ручки `GET /push/config`, `GET|POST|DELETE /push/subscriptions`, лимиты
10 подписок и 20 уведомлений в минуту; ключ p256dh проверяется как настоящая
точка P-256;
- установщик генерирует `VAPID_PRIVATE_KEY` (openssl, PKCS#8 DER в base64) и
`VAPID_SUBJECT`, ключ переиспользуется при переустановке; для ручной установки
есть `glchat vapid-keys`; `features.web_push_enabled` виден в `/meta`.
Тесты: правила и VAPID, доставка с поддельным push-эндпоинтом (шифрование,
заголовки VAPID/TTL/Topic, очистка мёртвых подписок), ручки и лимиты, отправка
при личном сообщении и упоминании, пропуск онлайн-получателей, миграция на
чистой БД.
AGENT.md 7.10 требует, чтобы действия с сообщениями были в журнале сервера, а
§11.5 перечисляет удаление и закрепление как действия инстанс-админа на чужом
сервере. Обе ручки рассылали только события Gateway, записей в аудите не было.
`message.edit` намеренно не пишется: история правок не хранится (7.6), а
правка своего сообщения — не модерационное действие и не входит в §11.5.
Личные беседы в аудит не попадают: у них нет сервера, а журнал ведётся по
серверам (7.10).
Тест: internal/server/audit_messages_test.go — удаление, pin и unpin чужого
сообщения инстанс-админом помечены `actor_instance_admin`, действие владельца
в журнале есть, но без флага; личные сообщения в аудит не пишутся.
- middleware OriginCheck: state-changing запросы с чужого Origin отклоняются
(AGENT.md 9.7); запросы без Origin пропускаются — cookie уже SameSite=Lax
- CSP перечисляет директивы явно (script-src/worker-src/manifest-src 'self',
style-src с inline для React, img-src с внешними https для аватаров вебхуков,
connect-src с доменом файлов и LiveKit); добавлены Permissions-Policy,
Cross-Origin-Opener-Policy и X-Permitted-Cross-Domain-Policies
- лимиты по AGENT.md 8.6: загрузки 10/мин и 100/сутки (вложения и аватары),
реакции 20/мин; администратор инстанса лимиты обходит
- step-up: смена прав роли (в теле PATCH) и удаление сервера (перед вызовом
/auth/step-up) требуют свежего подтверждения личности
- тесты: Origin (свой/чужой/GET), состав CSP, лимит реакций, step-up на права
роли; исправлен вызов NewRateLimiter (второй аргумент — burst, не окно)
- PUT/DELETE /users/@me/blocks/{id}: блокировка снимает дружбу и заявки в обе
стороны, разблокировка возвращает возможность писать (AGENT.md 7.2)
- блокировка запрещает личные сообщения (ошибка dm.blocked), открытие беседы и
заявки в друзья с любой стороны
- клиент: раздел «Заблокированные» в друзьях со снятием блокировки, действие
«Заблокировать» в меню строки друга и в меню модерации участника
- тесты: Go (блокировка, заявки, беседа, сообщение) и Vitest (список, снятие,
блокировка из меню)
- миграция 00012: таблица webhooks, снимок имени и аватара в messages (AGENT.md 7.11)
- API: список/создание/правка/удаление и пересоздание токена (MANAGE_WEBHOOKS,
step-up при создании, аудит), загрузка аватара отдельной multipart-ручкой
- исполнение POST /webhooks/{id}/{token} без сессии: content, username,
avatar_url, файлы создателя вебхука, лимит 30/мин на вебхук
- клиент: пункт настроек «Комната» со списком вебхуков, копированием ссылки,
пересозданием токена и удалением
- сообщения вебхуков: имя, аватар и значок в ленте вместо «неизвестного автора»
- fix: неполный ответ REST больше не затирает данные READY-снапшота
AGENT.md 7.6, 7.12:
- таблица `guild_emojis` (миграция 00008), загрузка PNG/JPEG/WebP/GIF до 512 КБ
с правом MANAGE_EMOJIS, имя `[a-zA-Z0-9_]{2,32}`, лимит 100 на сервер;
- ручки: список эмодзи сервера, переименование, удаление (с удалением файла),
загрузка multipart; аудит emoji.create/update/delete и событие
GUILD_EMOJIS_UPDATE всем участникам сервера;
- кастомные эмодзи в READY (`guilds[].emojis` с токеном `<:name:id>`), реакции
кастомными эмодзи проверяются по файлу, неизвестный эмодзи — 422;
- файлы эмодзи доступны авторизованным (используются в чужих сообщениях);
- системные сообщения: создание комнаты, вход участника (по приглашению и в
публичный сервер) и исключение участника пишутся в ленту типом `system`;
- тесты: права на загрузку, список, реакция кастомным эмодзи, переименование и
удаление, фильтрация системных записей в проверках истории.
AGENT.md 7.6:
- команды разбираются на сервере: /me (блок действия), /whisper и /wisper
(шёпот), /scream (громкий блок), «//» экранирует слэш, неизвестная команда
остаётся обычным текстом, пустой текст у команды — 422;
- /ls <логин> <текст> — личное сообщение: тип private, адресат в mentions,
получатель проверяется на участие в сервере и доступ к комнате; сообщение не
отдаётся посторонним ни в истории, ни в поиске (и FTS, и подстрочном) и
доставляется в Gateway только автору и адресатам;
- лимиты: 10 правок в минуту, 20 уникальных реакций на сообщение, 50
закреплений на комнату, не более 10 упоминаний, @everyone/@here только с
MENTION_EVERYONE;
- тесты: команды и экранирование, изоляция личного сообщения (история, поиск,
третий участник), лимит правок и @everyone.
Поиск требовал ввести слово целиком. Теперь:
- FTS5 ищет по началу слова: «пр» находит «Привет» (последнее слово запроса
получает префикс), регистр не важен;
- добавлена нормализованная копия текста (`messages.content_lower`, миграция
00007): подстрочные совпадения ищутся через LIKE без учёта регистра, включая
кириллицу (SQLite lower() её не приводит — регистр опускает приложение);
- при старте индекс дополняется у старых сообщений (порциями по 500);
- результаты FTS и LIKE объединяются без дублей, сортировка по новизне;
- тест: «пр» и «ивет» находят все три сообщения с «Привет/приветствие/ПРИВЕТ»,
посторонний текст не попадает.
По запросу пользователя (вне очереди AGENT.md §13):
- главного сервера как точки входа больше нет: новичок начинает с пустым
списком серверов, вход — только по приглашению или созданием своего;
- друзья: поиск по логину с экранированием LIKE, заявки (POST
/users/@me/relationships), принятие, удаление/отклонение, списки friends/
incoming/outgoing/blocked;
- личные беседы: POST /users/@me/channels (идемпотентно), GET
/users/@me/channels со собеседником, статусом и последним сообщением;
сообщения в DM работают через общие ручки комнат, доступ — только участникам
(посторонний получает 404, события в Gateway тоже фильтруются);
- присутствие: last_seen_at обновляется при активности, «невидимка» и простой
дольше двух минут выглядят как офлайн, смена статуса рассылает
PRESENCE_UPDATE друзьям;
- READY отдаёт dm_channels (собеседник, аватар, статус, последнее сообщение);
- профиль: timezone (по умолчанию Europe/Moscow) в PATCH /users/@me,
загрузка аватара POST /users/@me/avatar (проверка, что это изображение, в том
числе по содержимому) и удаление DELETE /users/@me/avatar; старый файл
удаляется с диска;
- тесты: заявки в друзья, личные беседы и их изоляция, «невидимка», часовой
пояс и аватар, PRESENCE_UPDATE другу, отсутствие событий DM у постороннего.
- упоминания в сообщении увеличивают счётчик упоминаний комнаты у упомянутых
(store.BumpMentionCount) и отправляют им READ_STATE_UPDATE на все устройства;
- отправка сообщения отмечает его прочитанным для автора;
- подтверждение прочтения сбрасывает упоминания;
- суточная квота 10 приглашений на пользователя (AGENT.md 8.6), администратор
инстанса её обходит; httpx.NewRateLimiterWindow для произвольных окон;
- тест на рост и сброс счётчика упоминаний.
Бэкенд текстовой связи (AGENT.md 7.6, 7.15, 7.16, 8.6):
- POST/GET/PATCH/DELETE /channels/{id}/messages, ответы и упоминания,
окно правки 24 часа, права автора или MANAGE_MESSAGES;
- реакции (PUT/DELETE .../reactions/{emoji}) с агрегацией и отметкой «моя»;
- закрепления (GET/PUT/DELETE .../pins) под MANAGE_MESSAGES;
- typing с лимитом 1/3 с и адресной рассылкой всем, кроме автора;
- read states: POST /channels/{id}/ack, синхронизация READ_STATE_UPDATE между
устройствами пользователя;
- поиск FTS5 по комнате (10/мин) с экранированием запроса;
- лимиты отправки 5/5 с (burst 10) и slowmode комнаты; администратор инстанса
обходит и то, и другое (AGENT.md 7.19);
- Gateway: DispatchToChannel/DispatchToChannelExcept доставляют события комнат
только тем, кто видит комнату (VIEW_CHANNEL), права считает общий движок;
- store: messages, message_reactions, channel_read_states, files (загрузка
файлов появится вместе с вложениями).
Тесты: жизненный цикл сообщения, видимость скрытой комнаты (404 участнику,
200 админу), slowmode, упоминания и ответы, typing и read state, фильтрация
событий комнаты между двумя WS-клиентами.