--- 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`. Возможно, вы тоже кормите чёрную дыру.