- Python 93.2%
- HTML 6.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Revert an earlier experiment that tied actor_uri to RELAY_ACTOR_NAME
(/actor/{name}) as a stale-key escape hatch — backfired on GoToSocial,
which enforces inbox uniqueness per remote account, so a second actor
URI sharing the same /inbox broke dereferencing of the new URI instead
of fixing anything. Back to the permanent, bare /actor path. The
relay's keypair also moves from a DB table to PEM files in DATA_DIR,
independent of the followers/dedupe database.
Three phases on top of that, driven by the unfiltered firehose
overwhelming a real follower's rate limits in practice:
- Only Application/Service-type actors may Follow (rejects individual
Person accounts outright) — otherwise one person's Follow gets the
same shared_inbox treatment as a real instance subscription, letting
a mistake or bad actor flood their whole instance.
- Per-follower filtering (filter_mode/filter_tags/filter_instances/
filter_exclude_bots) — tags (with prefix-match wildcard) or instances
match, bots excluded as an independent gate. filter_mode='all' (full
unfiltered firehose) is reachable only via a direct DB edit, never
any UI/API/DM path, after an earlier version's UI let a misclick
flood a live follower.
- Self-service filter config via ActivityPub DM: a follower's admin,
added to a manually-curated per-follower allowlist (acct handle +
WebFinger-resolved actor URI), can DM the relay tags:/add-tags:/
instances:/exclude-bots:/status/help commands. A separate operator
identity (ADMIN_ACTOR_URI) gets add-admin:/remove-admin:/list-admins:
across any follower's domain, on top of (not instead of) that same
identity's own per-follower commands.
Also fixes several GTS interop gaps found via live testing against a
real instance: outbound GETs (actor/key docs, WebFinger) now need to
be HTTP-signed, since GTS 401s unauthenticated fetches of a locked
account's actor document; httpx needs follow_redirects=True, since GTS
"split domain" accounts delegate WebFinger to the real instance host
via a plain redirect that httpx doesn't follow by default; DM replies
need an explicit Mention tag, since GTS silently drops a Create{Note}
addressed only via `to` to an account that doesn't follow the sender;
and DM command parsing needs to strip the leading @mention markup
real clients prepend to replies, or no command ever matches.
Assisted-By: Claude <noreply@anthropic.com>
|
||
| app | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| compose.yaml | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
relay
A small, self-hosted, manually-gated ActivityPub relay. It consumes
relay.fedi.buzz's public Mastodon-streaming
firehose (https://fedi.buzz/api/v1/streaming/public) and rebroadcasts posts
from it as Announce activities to whichever servers you've approved as
followers. The firehose itself is unfiltered by tag/instance/language, but
each follower can be scoped down to a subset of it — see
Per-follower filtering below.
Unlike old ActivityRelay-style (LitePub) relays, this one uses the same
one-way, bare-Announce-object pattern as buzzrelay/AodeRelay/rel.re,
which is what makes it work with GoToSocial.
How it works
- Peers (e.g. your GoToSocial instance) send a
Followto this relay's actor, from their instance-level federation actor — onlyApplication/Service-type actors are accepted; aFollowfrom an individual person's account is dropped outright, so a mistaken or malicious personal-account Follow can never point the full firehose at someone's whole instance. It shows up as pending in the admin UI — nothing is auto-accepted. - You manually Accept or Reject each request at
/admin/requests. - Once accepted, posts from the firehose matching that follower's filter
(see below — defaults to a filtered/empty state, never the full firehose,
until configured) are
Announced to their inbox, skipping posts originating from that follower's own domain. - Remove a follower any time at
/admin/followers— noUndoneeded on our side since we never Follow back.
Per-follower filtering
Each follower can be scoped to a subset of the firehose instead of
everything, configured per-row in /admin/followers:
- Tags — comma-separated, trailing
*for a prefix match (selfhost*matchesselfhosted,selfhosting, ...; without it, only that exact tag matches). - Instances — comma-separated origin hostnames.
- Exclude bot accounts — drops posts whose author is flagged
botby the firehose, regardless of tags/instances.
A post is delivered if it matches the tags or the instances (not both
required), unless bots are excluded, which always applies on top. The full
unfiltered firehose (all mode) is intentionally not reachable
through the admin UI or any DM command — after a UI misclick once flooded a
real follower's instance, granting it was made a direct database edit only,
done deliberately by you, never a routine toggle.
Self-service via DM
A follower's own admin can adjust their filter by sending the relay actor a private mention (not a public reply) with commands like:
tags: selfhost*, art
add-tags: gotosocial
remove-tags: art
instances: mastodon.social, fosstodon.org
exclude-bots: on
status
help
Only axes actually mentioned change; the rest keep their current value.
This only works for admins you've explicitly approved per-follower (web UI,
or by DMing add-admin: <domain> <acct_handle> if you're configured as the
operator via ADMIN_ACTOR_URI — see .env.example) — an unapproved sender
is silently ignored, no reply, so the relay never confirms or denies who's
approved to an outsider.
Running
cp .env.example .env # set RELAY_DOMAIN to your real public hostname
docker compose up -d
No Dockerfile/image build step — compose.yaml runs uv's own image directly
against the source bind-mounted from this directory (uv sync happens on
container start), same pattern as the poster project.
The relay actor will be reachable at https://$RELAY_DOMAIN/actor. Point
GoToSocial's Admin → Federation → relay-subscribe flow at that URL (or the
WebFinger form acct:relay@$RELAY_DOMAIN), then approve the resulting
follow request in /admin/requests.
Important — proxy exposure: /actor, /.well-known/webfinger, and
/inbox must stay publicly reachable with no auth (remote servers hit them
directly). Everything under /admin/* and / should sit behind your
reverse proxy's auth (e.g. Pangolin) — there is no app-level login.
Local dev
uv run uvicorn app.main:app --reload
Data
Follower state lives in a single SQLite file at $DATA_DIR/relay.db (WAL
mode); the relay's own signing keypair lives alongside it as
$DATA_DIR/relay_private_key.pem / relay_public_key.pem. Default
$DATA_DIR is ./data — since the whole project directory is bind-mounted
into the container, this is just a regular directory on the host. That's
the only thing you need to back up.