Files
dedinit.ru/content/posts/20260913 - gotosocial relay match by default/index.md
T

164 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
date: '2026-09-13T03:00:00+03:00'
lastmod: '2026-09-13T03:00:00+03:00'
draft: false
title: 'GoToSocial молча съедал все статусы с релеев — и я нашёл почему'
slug: 'gotosocial-relay-match-by-default'
description: 'Подписка на релей выглядит рабочей: релей шлёт, нода отвечает 202 Accepted — а в базе пусто. Разбираюсь, почему: без галочки Match posts by default и пустых матчерах фильтр релеев GoToSocial по построению возвращает false, и статусы дропаются молча на уровне debug.'
categories:
- 'DevOps'
- 'Federation'
tags:
- 'gotosocial'
- 'activitypub'
- 'fediverse'
- 'relay'
- 'mastodon'
- 'admin'
keywords:
- 'GoToSocial'
- 'ActivityPub'
- 'relay subscription'
- 'match by default'
- 'matchedByConnection'
- 'relay_subscriptions'
- '202 Accepted'
- 'dereference'
- 'dropping unpermitted'
- 'deny-by-default'
cover:
image: "hero.svg"
alt: "GoToSocial: релей молча съедает статусы — match_by_default"
---
# GoToSocial молча съедал все статусы с релеев — и я нашёл почему
**TL;DR:** Подписка на релей выглядит рабочей, релей шлёт, нода отвечает `202 Accepted` — а в базе пусто. Причина — **я не поставил галочку `Match posts by default`**, а матчеры оставил пустыми. В таком виде фильтр релеев по построению возвращает false для всего. Статусы дропаются молча, на уровне debug. Ошибка тихая: её заслоняют громкие ошибки dereference.
## Как это выглядит снаружи
Ты подписан на релей. Ты видишь:
- подписка активна, `approved = true`,
- релей исправно шлёт Announce,
- нода отвечает `202 Accepted`.
Всё зелёное. А лента месяцами состоит из пары доменов, на которые ты подписан напрямую. Релей как будто «работает вхолостую».
## ❌ Моя ошибка
Я настроил подписку так:
- ✅ разрешил **public**
- ✅ разрешил **unlisted**
- ✅ запретил **sensitive**
И решил, что этого достаточно. Логика была: «я разрешил то, что хочу, и запретил то, что не хочу — значит, всё остальное будет приходить само».
**Но это не так.** Разрешение public/unlisted и запрет sensitive — это **фильтры видимости**. Они говорят, *какие типы постов можно принимать*. Но они **не дают разрешения на приём вообще**. Разрешение даёт либо `match_by_default`, либо include-матчеры. У меня не было ни того, ни другого.
**Я не поставил галочку `Match posts by default`.** Без неё подписка работает в режиме deny-by-default: пропускает только то, что явно разрешено матчерами. Матчеров нет → не проходит **ничего**.
## Что происходит под капотом
1. Релей шлёт `POST /inbox` → нода отвечает `202 Accepted`. **Это подтверждение приёма HTTP-запроса, а не сохранения статуса.**
2. Нода скачивает оригинал по URI (dereference), тратит трафик и время.
3. Статус идёт в `relay.Filter.MatchedBySubscription`.
4. Нет совпадения → статус выбрасывается. В лог падает `dropping unpermitted status` — **warn/debug, не error**.
## Где прячется грабль
В таблице `relay_subscriptions` два ключевых поля: `flags` (битовая маска) и `matchers` (JSON-правила).
Флаги из `gtsmodel/relay.go`:
```
RelayFlagPublic = 2 (принимать публичные)
RelayFlagUnlisted = 4 (принимать скрытые)
RelayFlagMatchByDefault = 8 (принимать всё по умолчанию)
RelayFlagIgnoreSensitive = 16 (игнорировать чувствительное)
RelayFlagIgnoreMedia = 32 (игнорировать с медиа)
RelayFlagIgnoreReplies = 64 (игнорировать ответы)
```
У меня на всех трёх подписках стояло `flags = 22`. Раскладываем: `16 + 4 + 2` = `IgnoreSensitive + Unlisted + Public`. Выглядит осмысленно, правда? «Принимаем публичные и скрытые, игнорируем чувствительное».
Но бита `MatchByDefault` (8) там нет. И `matchers = NULL`.
## Почему без match_by_default дропается всё
Логика `matchedByConnection` в `internal/filter/relay/relay.go`:
1. Видимость: public → нужен флаг Public (есть), unlisted → нужен Unlisted (есть), остальное → false.
2. Чувствительное + `IgnoreSensitive` → false (это намеренно).
3. Медиа + `IgnoreMedia` → false (не стоит).
4. Ответ не себе + `IgnoreReplies` → false (не стоит).
5. Exclude-матчеры (чёрный список). Их нет → пропускаем.
6. **Если стоит `MatchByDefault` → true. У меня не стоит.**
7. Иначе ищем include-матчеры (белый список). Их нет вообще.
8. `return false`.
Вот оно. Пустая подписка без матчеров — это подписка, которая **не пропускает ничего**. Deny-by-default. Не «не знаю», а именно «не разрешаю».
## Что такое матчеры и зачем они нужны
Матчер — это ключевое слово, которое ищется в **содержимом поста и в его content warning**. Поиск регистронезависимый. Есть два режима совпадения: partial (по умолчанию, ловит часть слова) и whole word (только целое слово). Хэштеги матчатся через префикс `#`.
Матчеры бывают двух типов:
**Include-матчеры (белый список).** Работают, когда `match_by_default` выключен. Пост пройдёт только если совпал хотя бы с одним include-матчером. Нет include-матчеров и нет `match_by_default` — не пройдёт ничего. Именно это и случилось у меня.
**Exclude-матчеры (чёрный список).** Работают всегда, независимо от `match_by_default`. Если пост совпал с exclude-матчером — он дропается, даже если `match_by_default` включён.
Матчеры дают админу **хирургический контроль** вместо грубого «всё или ничего». Include — «хочу только посты про infosec и Linux». Exclude — «принимай всё, кроме спама и nsFW».
## Как диагностировать
SQL:
```sql
SELECT relay_actor_uri, flags, matchers FROM relay_subscriptions;
-- flags без бита 8 и matchers = NULL → подписка не пропускает ничего
```
API:
```
GET /api/v1/admin/relay_subscriptions
→ смотрим match_by_default
```
## ✅ Как чинить — и что надо было сделать сразу
Два корректных пути при добавлении релея:
**Путь 1: «Принимать всё, кроме явных запретов»** — поставить галочку `Match posts by default`. Тогда работают только exclude-матчеры и ignore-флаги. Всё, что не попало под запрет, — принимается.
**Путь 2: «Принимать только то, что я явно указал»** — не ставить `Match posts by default`, но создать include-матчеры. Например, `infosec`, `linux`, `#GoToSocial`.
Я выбрал ни то, ни другое. Надо было поставить галочку:
```
PUT /api/v1/admin/relay_subscriptions/{id}
{ "public": true, "unlisted": true, "match_by_default": true }
```
**После включения приток пошёл мгновенно:** 44 статуса за 30 минут, 12+ новых доменов (infosec.exchange, mastodon.world, burningboard.net, troet.cafe, norden.social, c.im, toot.wales, social.linux.pizza…). До этого лента месяцами содержала только пару доменов прямых подписок.
## Выводы
1. **Главное правило:** если хочешь «принимать всё, кроме запрещённого» — **обязательно ставь галочку `Match posts by default`**. Без неё релей будет слать, нода будет отвечать `202 Accepted`, а лента останется пустой.
2. Разрешить public/unlisted недостаточно — это лишь фильтры видимости, а не разрешение на приём. «Подписка есть» и «релей шлёт» ≠ «контент сохраняется».
3. Дизайн фильтра разумный — безопасный дефолт. Но для админа неочевидный: UI не кричит, что подписка без матчеров мёртвая.
4. Мониторь не только error, но и warn-строки `dropping unpermitted` / `not relayable`.
5. `202 Accepted` — это «запрос принят», а не «статус сохранён». Путать их — самый дешёвый способ незаметно потерять федерацию.
---
**Теги:** #GoToSocial #ActivityPub #Fediverse #Relay #администрирование #грабли
Если у кого-то была та же тишина в ленте при живом релее — проверьте `match_by_default`. Возможно, вы тоже кормите чёрную дыру.