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: пропускает только то, что явно разрешено матчерами. Матчеров нет → не проходит ничего.
Что происходит под капотом
- Релей шлёт
POST /inbox→ нода отвечает202 Accepted. Это подтверждение приёма HTTP-запроса, а не сохранения статуса. - Нода скачивает оригинал по URI (dereference), тратит трафик и время.
- Статус идёт в
relay.Filter.MatchedBySubscription. - Нет совпадения → статус выбрасывается. В лог падает
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:
- Видимость: public → нужен флаг Public (есть), unlisted → нужен Unlisted (есть), остальное → false.
- Чувствительное +
IgnoreSensitive→ false (это намеренно). - Медиа +
IgnoreMedia→ false (не стоит). - Ответ не себе +
IgnoreReplies→ false (не стоит). - Exclude-матчеры (чёрный список). Их нет → пропускаем.
- Если стоит
MatchByDefault→ true. У меня не стоит. - Иначе ищем include-матчеры (белый список). Их нет вообще.
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:
| |
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…). До этого лента месяцами содержала только пару доменов прямых подписок.
Выводы
- Главное правило: если хочешь «принимать всё, кроме запрещённого» — обязательно ставь галочку
Match posts by default. Без неё релей будет слать, нода будет отвечать202 Accepted, а лента останется пустой. - Разрешить public/unlisted недостаточно — это лишь фильтры видимости, а не разрешение на приём. «Подписка есть» и «релей шлёт» ≠ «контент сохраняется».
- Дизайн фильтра разумный — безопасный дефолт. Но для админа неочевидный: UI не кричит, что подписка без матчеров мёртвая.
- Мониторь не только error, но и warn-строки
dropping unpermitted/not relayable. 202 Accepted— это «запрос принят», а не «статус сохранён». Путать их — самый дешёвый способ незаметно потерять федерацию.
Теги: #GoToSocial #ActivityPub #Fediverse #Relay #администрирование #грабли
Если у кого-то была та же тишина в ленте при живом релее — проверьте match_by_default. Возможно, вы тоже кормите чёрную дыру.