<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Mastodon on dedinit()</title><link>https://dedinit.ru/tags/mastodon/</link><description>Recent content in Mastodon on dedinit()</description><generator>Hugo -- 0.165.0</generator><language>ru-ru</language><lastBuildDate>Sun, 13 Sep 2026 03:00:00 +0300</lastBuildDate><atom:link href="https://dedinit.ru/tags/mastodon/index.xml" rel="self" type="application/rss+xml"/><item><title>GoToSocial молча съедал все статусы с релеев — и я нашёл почему</title><link>https://dedinit.ru/2026/gotosocial-relay-match-by-default/</link><pubDate>Sun, 13 Sep 2026 03:00:00 +0300</pubDate><guid>https://dedinit.ru/2026/gotosocial-relay-match-by-default/</guid><description>Подписка на релей выглядит рабочей: релей шлёт, нода отвечает 202 Accepted — а в базе пусто. Разбираюсь, почему: без галочки Match posts by default и пустых матчерах фильтр релеев GoToSocial по построению возвращает false, и статусы дропаются молча на уровне debug.</description><content:encoded><![CDATA[<figure class="entry-cover"><img src="https://dedinit.ru/2026/gotosocial-relay-match-by-default/hero.svg" alt="GoToSocial: релей молча съедает статусы — match_by_default"></figure><h1 id="gotosocial-молча-съедал-все-статусы-с-релеев--и-я-нашёл-почему">GoToSocial молча съедал все статусы с релеев — и я нашёл почему</h1>
<p><strong>TL;DR:</strong> Подписка на релей выглядит рабочей, релей шлёт, нода отвечает <code>202 Accepted</code> — а в базе пусто. Причина — <strong>я не поставил галочку <code>Match posts by default</code></strong>, а матчеры оставил пустыми. В таком виде фильтр релеев по построению возвращает false для всего. Статусы дропаются молча, на уровне debug. Ошибка тихая: её заслоняют громкие ошибки dereference.</p>
<h2 id="как-это-выглядит-снаружи">Как это выглядит снаружи</h2>
<p>Ты подписан на релей. Ты видишь:</p>
<ul>
<li>подписка активна, <code>approved = true</code>,</li>
<li>релей исправно шлёт Announce,</li>
<li>нода отвечает <code>202 Accepted</code>.</li>
</ul>
<p>Всё зелёное. А лента месяцами состоит из пары доменов, на которые ты подписан напрямую. Релей как будто «работает вхолостую».</p>
<h2 id="-моя-ошибка">❌ Моя ошибка</h2>
<p>Я настроил подписку так:</p>
<ul>
<li>✅ разрешил <strong>public</strong></li>
<li>✅ разрешил <strong>unlisted</strong></li>
<li>✅ запретил <strong>sensitive</strong></li>
</ul>
<p>И решил, что этого достаточно. Логика была: «я разрешил то, что хочу, и запретил то, что не хочу — значит, всё остальное будет приходить само».</p>
<p><strong>Но это не так.</strong> Разрешение public/unlisted и запрет sensitive — это <strong>фильтры видимости</strong>. Они говорят, <em>какие типы постов можно принимать</em>. Но они <strong>не дают разрешения на приём вообще</strong>. Разрешение даёт либо <code>match_by_default</code>, либо include-матчеры. У меня не было ни того, ни другого.</p>
<p><strong>Я не поставил галочку <code>Match posts by default</code>.</strong> Без неё подписка работает в режиме deny-by-default: пропускает только то, что явно разрешено матчерами. Матчеров нет → не проходит <strong>ничего</strong>.</p>
<h2 id="что-происходит-под-капотом">Что происходит под капотом</h2>
<ol>
<li>Релей шлёт <code>POST /inbox</code> → нода отвечает <code>202 Accepted</code>. <strong>Это подтверждение приёма HTTP-запроса, а не сохранения статуса.</strong></li>
<li>Нода скачивает оригинал по URI (dereference), тратит трафик и время.</li>
<li>Статус идёт в <code>relay.Filter.MatchedBySubscription</code>.</li>
<li>Нет совпадения → статус выбрасывается. В лог падает <code>dropping unpermitted status</code> — <strong>warn/debug, не error</strong>.</li>
</ol>
<h2 id="где-прячется-грабль">Где прячется грабль</h2>
<p>В таблице <code>relay_subscriptions</code> два ключевых поля: <code>flags</code> (битовая маска) и <code>matchers</code> (JSON-правила).</p>
<p>Флаги из <code>gtsmodel/relay.go</code>:</p>
<pre tabindex="0"><code>RelayFlagPublic          = 2   (принимать публичные)
RelayFlagUnlisted        = 4   (принимать скрытые)
RelayFlagMatchByDefault  = 8   (принимать всё по умолчанию)
RelayFlagIgnoreSensitive = 16  (игнорировать чувствительное)
RelayFlagIgnoreMedia     = 32  (игнорировать с медиа)
RelayFlagIgnoreReplies   = 64  (игнорировать ответы)
</code></pre><p>У меня на всех трёх подписках стояло <code>flags = 22</code>. Раскладываем: <code>16 + 4 + 2</code> = <code>IgnoreSensitive + Unlisted + Public</code>. Выглядит осмысленно, правда? «Принимаем публичные и скрытые, игнорируем чувствительное».</p>
<p>Но бита <code>MatchByDefault</code> (8) там нет. И <code>matchers = NULL</code>.</p>
<h2 id="почему-без-match_by_default-дропается-всё">Почему без match_by_default дропается всё</h2>
<p>Логика <code>matchedByConnection</code> в <code>internal/filter/relay/relay.go</code>:</p>
<ol>
<li>Видимость: public → нужен флаг Public (есть), unlisted → нужен Unlisted (есть), остальное → false.</li>
<li>Чувствительное + <code>IgnoreSensitive</code> → false (это намеренно).</li>
<li>Медиа + <code>IgnoreMedia</code> → false (не стоит).</li>
<li>Ответ не себе + <code>IgnoreReplies</code> → false (не стоит).</li>
<li>Exclude-матчеры (чёрный список). Их нет → пропускаем.</li>
<li><strong>Если стоит <code>MatchByDefault</code> → true. У меня не стоит.</strong></li>
<li>Иначе ищем include-матчеры (белый список). Их нет вообще.</li>
<li><code>return false</code>.</li>
</ol>
<p>Вот оно. Пустая подписка без матчеров — это подписка, которая <strong>не пропускает ничего</strong>. Deny-by-default. Не «не знаю», а именно «не разрешаю».</p>
<h2 id="что-такое-матчеры-и-зачем-они-нужны">Что такое матчеры и зачем они нужны</h2>
<p>Матчер — это ключевое слово, которое ищется в <strong>содержимом поста и в его content warning</strong>. Поиск регистронезависимый. Есть два режима совпадения: partial (по умолчанию, ловит часть слова) и whole word (только целое слово). Хэштеги матчатся через префикс <code>#</code>.</p>
<p>Матчеры бывают двух типов:</p>
<p><strong>Include-матчеры (белый список).</strong> Работают, когда <code>match_by_default</code> выключен. Пост пройдёт только если совпал хотя бы с одним include-матчером. Нет include-матчеров и нет <code>match_by_default</code> — не пройдёт ничего. Именно это и случилось у меня.</p>
<p><strong>Exclude-матчеры (чёрный список).</strong> Работают всегда, независимо от <code>match_by_default</code>. Если пост совпал с exclude-матчером — он дропается, даже если <code>match_by_default</code> включён.</p>
<p>Матчеры дают админу <strong>хирургический контроль</strong> вместо грубого «всё или ничего». Include — «хочу только посты про infosec и Linux». Exclude — «принимай всё, кроме спама и nsFW».</p>
<h2 id="как-диагностировать">Как диагностировать</h2>
<p>SQL:</p>
<div class="highlight"><div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff7b72">SELECT</span><span style="color:#6e7681"> </span>relay_actor_uri,<span style="color:#6e7681"> </span>flags,<span style="color:#6e7681"> </span>matchers<span style="color:#6e7681"> </span><span style="color:#ff7b72">FROM</span><span style="color:#6e7681"> </span>relay_subscriptions;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">-- flags без бита 8 и matchers = NULL → подписка не пропускает ничего
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>API:</p>
<pre tabindex="0"><code>GET /api/v1/admin/relay_subscriptions
→ смотрим match_by_default
</code></pre><h2 id="-как-чинить--и-что-надо-было-сделать-сразу">✅ Как чинить — и что надо было сделать сразу</h2>
<p>Два корректных пути при добавлении релея:</p>
<p><strong>Путь 1: «Принимать всё, кроме явных запретов»</strong> — поставить галочку <code>Match posts by default</code>. Тогда работают только exclude-матчеры и ignore-флаги. Всё, что не попало под запрет, — принимается.</p>
<p><strong>Путь 2: «Принимать только то, что я явно указал»</strong> — не ставить <code>Match posts by default</code>, но создать include-матчеры. Например, <code>infosec</code>, <code>linux</code>, <code>#GoToSocial</code>.</p>
<p>Я выбрал ни то, ни другое. Надо было поставить галочку:</p>
<pre tabindex="0"><code>PUT /api/v1/admin/relay_subscriptions/{id}
{ &#34;public&#34;: true, &#34;unlisted&#34;: true, &#34;match_by_default&#34;: true }
</code></pre><p><strong>После включения приток пошёл мгновенно:</strong> 44 статуса за 30 минут, 12+ новых доменов (infosec.exchange, mastodon.world, burningboard.net, troet.cafe, norden.social, c.im, toot.wales, social.linux.pizza…). До этого лента месяцами содержала только пару доменов прямых подписок.</p>
<h2 id="выводы">Выводы</h2>
<ol>
<li><strong>Главное правило:</strong> если хочешь «принимать всё, кроме запрещённого» — <strong>обязательно ставь галочку <code>Match posts by default</code></strong>. Без неё релей будет слать, нода будет отвечать <code>202 Accepted</code>, а лента останется пустой.</li>
<li>Разрешить public/unlisted недостаточно — это лишь фильтры видимости, а не разрешение на приём. «Подписка есть» и «релей шлёт» ≠ «контент сохраняется».</li>
<li>Дизайн фильтра разумный — безопасный дефолт. Но для админа неочевидный: UI не кричит, что подписка без матчеров мёртвая.</li>
<li>Мониторь не только error, но и warn-строки <code>dropping unpermitted</code> / <code>not relayable</code>.</li>
<li><code>202 Accepted</code> — это «запрос принят», а не «статус сохранён». Путать их — самый дешёвый способ незаметно потерять федерацию.</li>
</ol>
<hr>
<p><strong>Теги:</strong> #GoToSocial #ActivityPub #Fediverse #Relay #администрирование #грабли</p>
<p>Если у кого-то была та же тишина в ленте при живом релее — проверьте <code>match_by_default</code>. Возможно, вы тоже кормите чёрную дыру.</p>
]]></content:encoded></item></channel></rss>