Owner decision 2026-10-04: SecureMode on once the pasture passes with it.
- Both clean pasture passes were run over all six peers:
- normally: 246 passed, 0 failed;
- with Federation__SecureMode=true: every federation check passed. The only failures were four checks expecting an
unsigned GET to get 404 or 410 where SecureMode answers 401. Those checks now go through `unserved` and
`gone_unsigned` (lib/interop.sh), which expect 401 when SecureMode is on.
- Circle posts now reach their member on GoToSocial and Mastodon, and survive Mastodon's signed refetch, as does a
followers-only post. The GoToSocial expected failure is gone.
- appsettings.Production.json turns SecureMode on.
- The deploy now checks that an unsigned GET of @thepra answers 401 and that a browser is redirected. It reads
@thepra's discoverability through the Mastodon API, since the actor is no longer readable unsigned.
- docs/INTEROP.md (Mastodon, GoToSocial), CLAUDE.md and ROADMAP updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
895 lines
63 KiB
Markdown
895 lines
63 KiB
Markdown
# Interop: what every peer sends, what it expects, and what PrivaPub still drops
|
||
|
||
Research as of **2026-10-01**: the platforms' source on their default branches, live ActivityPub fetches from large
|
||
instances, release notes and the FEP repository. Version numbers are what was current that day. Claims the
|
||
research could not confirm from a primary source are marked *(unconfirmed)*. The sources are at the end.
|
||
|
||
This file is for two readers:
|
||
- **Whoever changes federation code.** Every gotcha below has broken somebody.
|
||
- **Whoever builds the rich client.** That client shows every kind of fediverse content in one place, with a secondary
|
||
"details" view of the raw object and how it reached us. Section 4 says what the server must keep for it.
|
||
|
||
Priorities, used throughout:
|
||
|
||
| Priority | Meaning |
|
||
|---|---|
|
||
| **P1** | Breaks interop, or loses content the user would see. |
|
||
| **P2** | Degrades the experience. |
|
||
| **P3** | Nice to have. |
|
||
|
||
## 1. Where PrivaPub stands (checked in the code, 2026-10-01)
|
||
|
||
**Already right**
|
||
- **Public addressing:** all three spellings are recognised (`…#Public`, `as:Public`, `Public`).
|
||
- **Undo:** embeds its whole object, which Misskey needs; it fetches the object only when it isn't embedded.
|
||
- **Content types:**
|
||
- Served documents are `application/activity+json; charset=utf-8`, which is on Lemmy's exact allowlist and is
|
||
accepted by GoToSocial and Misskey.
|
||
- WebFinger answers `application/jrd+json`, and answers for the actor URL as well as `acct:`. Akkoma and
|
||
Iceshrimp.NET require both. A bare `user@domain` or `@user@domain` is taken as an `acct:` too, as Mastodon,
|
||
GoToSocial and Pleroma take it (2026-10-03).
|
||
- **SSRF guard:**
|
||
- Its IPv6 rule is an allowlist (`2000::/3` only), so IPv4-compatible `::a.b.c.d`, NAT64 and the other tricks behind
|
||
Mastodon's GHSA-vwhj (Jul 2026) are already refused.
|
||
- It caps fetches at 1 MB. Misskey caps at 256 KiB, so keep our own documents well under that.
|
||
- **Attachments:**
|
||
- Up to 16 are kept. Mastodon drops anything past 4; Pixelfed albums and Threads carousels go beyond it.
|
||
- `{type: Link}` attachments are not mistaken for media.
|
||
- **Deleted posts** answer 410 with a `Tombstone`. Actor `published` is truncated to the day. The `webfinger` property
|
||
(FEP-2c59) is on every actor.
|
||
- **Live check:** the whole follow, post, reply, like, boost, DM, edit and delete set round-trips with GoToSocial
|
||
0.22.1 (`tools/pasture/`).
|
||
|
||
**Wrong at the time of the research (P1, cheap; all three fixed in v1.7.0)**
|
||
- **`summary` is read as a content warning on every object type.** It is one only on a `Note`, or when
|
||
`sensitive: true` is set. Elsewhere it is something else:
|
||
|
||
| Sender | What `summary` holds |
|
||
|---|---|
|
||
| WordPress, WriteFreely, Ghost, NodeBB (`Article`) | a teaser or excerpt |
|
||
| Mobilizon, Gancio (`Event`) | the date and address, or the whole description |
|
||
| Funkwhale (`Audio`) | a line of hashtags |
|
||
| Mbin (`Page`) | a short title plus tags |
|
||
|
||
Today all of these arrive hidden behind a warning, and marked sensitive.
|
||
- **Persona and group usernames are only lowercased.** Mastodon accepts `[a-z0-9_]` with `.` and `-` only inside the
|
||
name; Misskey `^\w([\w-.]*\w)?$`, 128 characters at most. A persona named outside that is unreachable from either.
|
||
- **Our JSON-LD context does not define `postingRestrictedToMods`.** Iceshrimp.NET runs full JSON-LD expansion and
|
||
silently drops every undefined term. Any term we add later (`quote`, `Emoji`, `EmojiReact`, `interactionPolicy`,
|
||
`votersCount`) must be defined in `ActivityPubRenderer.Context()` in the same commit.
|
||
|
||
**Smaller**
|
||
- No `Vary: Accept` on actor and object URLs, although they answer HTML or JSON depending on `Accept`.
|
||
- Only `create-…` and `announce-…` activity ids dereference. `follow-`, `like-`, `accept-` and `undo-…` answer 404.
|
||
That is harmless while objects are embedded, but every id should resolve.
|
||
|
||
## 2. Rules that hold across platforms
|
||
|
||
| # | Rule | What PrivaPub must do | P |
|
||
|---|---|---|---|
|
||
| W1 | `url`, `icon`, `image`, `attributedTo`, `actor`, `tag`, `attachment` and `alsoKnownAs` can each be a single value, an object or an array. The `url` array matters most: PeerTube video files, Funkwhale streams, and Bridgy posts, whose `rel: canonical` link is `at://…`. | Parse every shape. For "open original", use the `text/html` Link. Keep every other Link as a media variant. | P1 |
|
||
| W2 | `summary` is a content warning only on a `Note`, or when `sensitive: true`. | See §1. On other types store it as an excerpt or description. NodeBB 4.16 honours a CW only on a sensitive Note. | P1 |
|
||
| W3 | `content` can be Markdown. PeerTube descriptions and comments carry `mediaType: text/markdown`. | Render it, then sanitise. Keep `source{content, mediaType}`: Markdown, BBCode, `text/x.misskeymarkdown` (MFM). | P1 |
|
||
| W4 | `mediaType` is missing or wrong: Bridgy media has none, Funkwhale hard-codes `audio/mpeg`, Gancio `image/jpeg`. Mastodon types every attachment `Document`. | Infer it from the object or attachment type, then sniff it in the media proxy. | P1 |
|
||
| W5 | Thumbnails live in five places: attachment `icon` (Mastodon), object `icon[]` (PeerTube), object `preview` (Loops), `image` (Bridgy video; Ghost as a bare string; WordPress), and `icon` (Plume). | Keep them all; choose one per kind. | P1 |
|
||
| W6 | `Accept`, `Reject` and `TentativeAccept` are not always follow answers. Friendica and Hubzilla use them as event RSVPs; Mobilizon answers a `Join`; GoToSocial answers interaction requests with `result`; Mastodon answers a `QuoteRequest`. | Route on what the object *is*. | P1 |
|
||
| W7 | `id` is not the page a person opens. WordPress uses `?p=123`, Ghost `/.ghost/…`, Bridgy `/convert/ap/at://…`. | Link to `url`, never to `id`. | P1 |
|
||
| W8 | Rich types are updated in place: PeerTube live state, WordPress (on every save), Mobilizon. A poll's counts are refreshed with an `Update{Question}` that changes nothing else. | Apply Updates to every kind. An Update with no newer `updated` is a refresh, never an edit revision. Mastodon applies the same rule. | P1 |
|
||
| W9 | A `Delete` can arrive before its `Create`, and relays and forwarders re-send old Creates. | Keep tombstones so deleted posts stay deleted. When the deleter is not the author, confirm with the origin: 404 or 410 means deleted. | P1 |
|
||
| W10 | Time comes in seconds (Mastodon), milliseconds (Misskey, us), or with offsets. `-00:00` means floating local time (Hubzilla events). `duration` is ISO 8601 (`PT6299S`). GoToSocial rejects a status whose `updated` is earlier than `published`. | Parse all of these, and clamp future times. Order by arrival (PrivacyIds.Arrived). Never emit `updated` < `published`. | P1 |
|
||
| W11 | Language may be in `contentMap`, in `@context[].@language` (Pleroma 2.9+), or a `language{identifier,name}` object (Lemmy, PeerTube). Akkoma replaces `content` with the *first* `contentMap` entry. | Read all three. When sending, put `content` and the primary `contentMap` entry first and keep them equal. | P2 |
|
||
| W12 | Alt text: `name` (Mastodon), `summary` (GoToSocial 0.20.0, Akkoma reads it first), or their `*Map` forms. Avatar and header alt is in `icon.name`/`image.name`, or `summary` on Mastodon. | Read both; send `name`. | P2 |
|
||
| W13 | `"id": null` objects (Akkoma); a `Tombstone` served with **200** as a soft delete (FEP-4f05: NodeBB, Discourse); 410 for deleted GoToSocial 0.22 statuses. | Accept a null id inside an activity; never emit one. Any `Tombstone`, whatever the status code, means deleted. | P2 |
|
||
| W14 | Size limits on the receiving side: Misskey reads at most 256 KiB, truncates text at 8192 characters, CW 512, poll choice 256, alt 512. GoToSocial takes emoji up to 100 KB. Peers cap fetches at about 1 MB. | Accept long posts from others. Keep our own documents small. | P2 |
|
||
| W15 | Hashtag `name` comes with or without `#`. Mastodon normalises with NFKC + lowercase (watch Turkish `İ`). Lemmy adds an automatic `#<community>` tag to every post. | Normalise the same way; ignore Lemmy's automatic tag. | P3 |
|
||
|
||
## 3. Per platform
|
||
|
||
### Mastodon: 4.7.2 (2026-09-15); mastodon.social runs 4.8 alpha; client `api_versions.mastodon` = 11
|
||
|
||
**Emits**
|
||
- **Objects and activities:**
|
||
- Objects: `Note` and `Question` only.
|
||
- Federated `Block`.
|
||
- `Add`/`Remove` for pins, featured hashtags and featured collections (4.6, FEP-7aa9).
|
||
- `Move`.
|
||
- `QuoteRequest`, its `Accept`/`Reject` (with `result`), and `Delete{QuoteAuthorization}`.
|
||
- `FeatureRequest`/`FeatureAuthorization` (4.6).
|
||
- **Threads:** `context` is a dereferenceable collection of the thread (FEP-7888, threads started on 4.5+).
|
||
- **Interaction counts:** `likes` and `shares` carry `totalItems`.
|
||
- **Quotes:** `quote`, plus `quoteUri` and `_misskey_quote`, `quoteAuthorization`, and `interactionPolicy.canQuote`.
|
||
- **Link previews:** from 4.7, a `{type: Link, href}` **attachment** names the link a card is made from (FEP-8967).
|
||
- **Attachments:** `duration`, and `icon` thumbnails.
|
||
- **Actors:**
|
||
- `webfinger`, `featuredCollections`, `interactionPolicy.canFeature`, `attributionDomains`;
|
||
- `memorial`, `suspended`, `indexable`, `discoverable`;
|
||
- avatar and header alt text in `summary`;
|
||
- several keys allowed (4.6).
|
||
- **Ids:** new accounts get numeric actor ids (4.5). Remote renames are accepted, keyed on the actor `id` (4.7).
|
||
|
||
**Expects**
|
||
- **Fetched documents:**
|
||
- `id` equals the URL requested, exactly.
|
||
- The type is `activity+json`, or `ld+json` with the profile.
|
||
- `@context` includes the ActivityStreams URL.
|
||
- **WebFinger:** it loops back to the same `id`.
|
||
- **draft-cavage signatures:**
|
||
- `(request-target)` includes the query string (4.3).
|
||
- `digest` is signed on POST and `host` on GET.
|
||
- The window is 12 h, with 1 h of skew.
|
||
- Mastodon no longer signs `Accept` (4.6). Never require it.
|
||
- **RFC 9421:** verified by default since 4.5.0.
|
||
- A request carries one signature only.
|
||
- It must cover `@method`, `@target-uri` and `content-digest`, and include `created` and `keyid`.
|
||
- **4.7 double-knocks:** it signs draft-cavage first, and if we answer 400 or 401 it retries with RFC 9421. A 400 for
|
||
any other reason therefore triggers a retry we would fail.
|
||
- Mastodon answers **503** when it temporarily cannot fetch a key; that is a retry, not a failure.
|
||
- **Edits:** an Update counts as an edit only with a newer `updated`.
|
||
- **Quotes:** a post with no `canQuote` **cannot be quoted** by Mastodon users at all. A quote without a valid stamp
|
||
stays "pending", and only its fallback link shows.
|
||
- **Fetch all replies** is always on (4.5). Mastodon crawls `replies` as its instance actor, up to 500 per post, and
|
||
**deletes its copy** when a refetch answers 404.
|
||
- **Converted types:** `Article`, `Page`, `Event`, `Video`, `Audio` and `Image` become `<h2>name</h2>` + summary +
|
||
link. **The body is dropped.** Only `Mention` tags notify. Previews come from fetching the page; FEP-8967's
|
||
`preview` is ignored.
|
||
|
||
**Gaps**
|
||
|
||
| Gap | P | Client surface |
|
||
|---|---|---|
|
||
| Quotes: read all keys; verify the `QuoteAuthorization` (type, host, `attributedTo`, `interactingObject`, `interactionTarget`; GHSA-vg36 compared domains only); revocation; strip the `.quote-inline` fallback | P1 | `Status.quote{state, quoted_status}`, `quotes_count`, `/statuses/:id/quotes`, `quote`/`quoted_update` notifications, `api_versions.mastodon ≥ 7` |
|
||
| Being quotable: emit `canQuote`; answer `QuoteRequest` with `Accept{object: request id, result: stamp}`; serve and revoke stamps | P2 | `Status.quote_approval`, `PUT /statuses/:id/interaction_policy` |
|
||
| Polls (see §3 Misskey for vote shapes) | P1 | `Status.poll`, `/polls/:id`, `/polls/:id/votes`, `poll` notification |
|
||
| Custom emoji on posts, names, fields and poll options, proxied, refreshed by `updated` | P1 | `Status.emojis`, `Account.emojis` |
|
||
| Link attachments as the card source | P1 | `Status.card` |
|
||
| Inbound `Move` with Mastodon's checks (`target` re-fetched, its `alsoKnownAs` lists the old account, 7-day lock); move each persona's follow | P1 | `Account.moved` |
|
||
| Re-run WebFinger when `preferredUsername` changes; key accounts on the actor id | P2 | `Account.acct` |
|
||
| Inbound `Block`: stop delivering, hide | P2 | `relationship.blocked_by` |
|
||
| `Add`/`Remove` featured (pins, tags) | P2 | `GET /accounts/:id/statuses?pinned=true` |
|
||
| Remote like and boost totals | P2 | counts |
|
||
| Publish `context` and a paged `replies` | P2 | — |
|
||
| `FeatureRequest`: send `Reject` (or implement FEP-7aa9) | P3 | — |
|
||
|
||
**Pasture evidence (2026-10-03, Mastodon v4.7.3, `tools/pasture/scenarios/mastodon.sh`):** 49 checks pass. They cover:
|
||
- discovery, follows and locked follows both ways;
|
||
- public, CW and followers-only posts (the last answering 404 unsigned);
|
||
- replies threading both ways;
|
||
- likes, boosts and their undos both ways, with counts;
|
||
- DMs both ways;
|
||
- polls and votes both ways;
|
||
- FEP-044f quotes approved both ways;
|
||
- images with alt text both ways, ours through `/media/proxy`;
|
||
- edits with history both ways;
|
||
- deletes both ways, ours answering 410;
|
||
- a Flag reaching Mastodon's moderators from the instance actor;
|
||
- a circle request held for its owner and approved;
|
||
- unfollow and block;
|
||
- statistics naming `mastodon.test` as mastodon with no account names.
|
||
|
||
Findings:
|
||
- **Mastodon 4.7 names its actors by number**: `https://mastodon.test/ap/users/<id>`, inbox `…/ap/users/<id>/inbox`. Nothing here may assume
|
||
`/users/<name>`.
|
||
- **It dropped circle posts** until each member's copy named that member (owner decision 2026-10-04, v1.19.0).
|
||
- A post addressed only to `[circle, circle/flock]` parses as `direct` there, and Mastodon keeps a `direct` post
|
||
only if it names a local account or arrived in a known account's inbox (`Create#addresses_local_accounts?`).
|
||
- But `ActivityPub::InboxesController#account_required?` looks only at `params[:account_username]`. A delivery to
|
||
the numeric `/ap/users/:account_id/inbox` it now advertises therefore reaches the worker with no recipient and
|
||
is rejected.
|
||
- DMs are unaffected because they name the recipient.
|
||
- Each member's copy now names that member in `cc` and mentions them silently, so Mastodon keeps it, and its signed
|
||
refetch (`ActivityPub::FetchRemoteStatusService`, by its instance actor) gets the post back naming the members on
|
||
that server. The same holds for a followers-only post's refetch, which used to answer 404, and a 404 on refetch
|
||
makes Mastodon delete its copy. Reporting the numeric-inbox recipient loss upstream is still worth doing.
|
||
- **With SecureMode on** (all six peers, 2026-10-04) every federation check passes: Mastodon, GoToSocial, Misskey,
|
||
Sharkey, Akkoma and Lemmy all sign their fetches, and fetch our instance actor's key unsigned first.
|
||
- Inbound `Block` from Mastodon is not enforced yet (P7).
|
||
|
||
### GoToSocial: 0.22.1 (2026-07-20)
|
||
|
||
**Emits**
|
||
- **Objects:** `Note` and `Question` only.
|
||
- **Interaction policies** on every status: `canLike`, `canReply`, `canAnnounce` and `canQuote`, each split into
|
||
`automaticApproval` and `manualApproval`.
|
||
- Defaults: open on public and unlisted posts.
|
||
- On followers-only posts, liking and replying are limited to the author, followers and mentioned accounts, and
|
||
boosting to the author.
|
||
- `canQuote` is author-only on every post (0.21).
|
||
- **Since 0.21 it asks politely:** `LikeRequest`, `ReplyRequest` and `AnnounceRequest`, with the interaction in
|
||
`instrument`. The answer is `Accept`/`Reject` whose `result` is a `*Authorization`. The interaction then carries
|
||
`replyAuthorization` (with `approvedBy` as the legacy fallback).
|
||
- **Keys and actors:**
|
||
- The key id is `…/main-key` (no `#`), and its document is a stub actor.
|
||
- There is **no `sharedInbox`**.
|
||
- `hidesToPublicFromUnauthedWeb` / `hidesCcPublicFromUnauthedWeb`, `indexable`.
|
||
- `featured` holds URIs only, and changes to it are never announced, so read it instead.
|
||
- **What it leaves out:**
|
||
- `context`;
|
||
- `likes`/`shares`;
|
||
- attachment `width`/`height`.
|
||
- **Deleted statuses:** 0.22 keeps a stub and answers 410.
|
||
|
||
**Expects**
|
||
- **Signed requests:** every GET and POST is signed, draft-cavage only, with RSA keys. **No RFC 9421 in either
|
||
direction.**
|
||
- **Key handshake:** the instance actor and key documents must be served **unsigned**, or both sides deadlock fetching
|
||
each other's keys. Ours are: SecureMode exempts the instance actor.
|
||
- **Content-Type:** an inbox POST must be `activity+json`, or `ld+json` with the profile. Anything else gets 406.
|
||
- **Activities:** one without an `id` is dropped. A 400 is never retried.
|
||
- **Keys:** a changed public key on refresh is refused. **Never rotate keys silently.**
|
||
- **Interaction policies:**
|
||
- Third-party GoToSocial servers drop replies that have no valid `replyAuthorization`.
|
||
- On followers-only GoToSocial posts, send `ReplyRequest` / `LikeRequest` instead of a bare Create or Like.
|
||
- **Rate limit:** 300 requests per 5 minutes per IP, answered with 503 and `Retry-After`.
|
||
- **Not accepted:** top-level `Audio`.
|
||
- **Timelines:** its cached home timeline can miss new posts. Check a delivery by URI, not through its timelines; see
|
||
CLAUDE.md, "Testing".
|
||
|
||
**Gaps**
|
||
|
||
| Gap | P | Client surface |
|
||
|---|---|---|
|
||
| Store remote `interactionPolicy` (with GoToSocial's defaults); disable or mark actions | P1 | GoToSocial-style `Status.interaction_policy` |
|
||
| Send `ReplyRequest`/`LikeRequest` where approval is needed; handle `Accept{result}`/`Reject`; attach the authorization | P1 | own: pending/approved/rejected on our own reply |
|
||
| Honour 503 with `Retry-After` in delivery and in the proxy | P1 | — |
|
||
| Respect `hides*FromUnauthedWeb` on our public pages; emit it for personas (it suits the privacy design) | P2 | — |
|
||
| Measure media size when proxying | P2 | `MediaAttachment.meta` |
|
||
| Only advertise the policies we enforce | P2 | — |
|
||
|
||
**Pasture evidence (2026-10-03, GoToSocial 0.22.1, `tools/pasture/scenarios/gts.sh`):** 37 checks pass, three runs in a
|
||
row. That is the original 33 plus four on statistics: described as gotosocial, inbound and outbound traffic counted,
|
||
no account named. Since 2026-10-04 (v1.19.0) circle posts reach a GoToSocial member too. GoToSocial files a post for
|
||
neither the public nor the author's followers as a direct message, like our DMs, and shows it only to the accounts it
|
||
mentions. Being in `cc` stored it but left it invisible, so each member's copy also mentions that member silently.
|
||
Such posts are then found in the member's conversations, never by a search on their URI.
|
||
|
||
### Misskey family: Misskey 2026.10.0, Sharkey 2025.4.7, Iceshrimp.NET 2026.1.2-beta, CherryPick 4.17
|
||
|
||
Firefish is dead (its site has answered 410 since February 2025).
|
||
|
||
**Emits**
|
||
- **Text and quotes:**
|
||
- `_misskey_content` plus `source{content, mediaType: text/x.misskeymarkdown}` (MFM).
|
||
- The quote is `_misskey_quote`/`quoteUrl`, plus a `RE:` fallback inside the content.
|
||
- An empty CW is a zero-width space.
|
||
- **Attachments:** each carries its own `sensitive`.
|
||
- **Emoji tags** carry `_misskey_license`.
|
||
- **Polls:** `Question` with `oneOf`/`anyOf`. Counts are in `replies.totalItems` per option. `endTime` and `closed` are
|
||
set; there is **no `votersCount`**.
|
||
- A vote is `Create{Note{name, inReplyTo, to:[owner]}}`, one per choice.
|
||
- Every vote triggers an `Update{Question}`.
|
||
- **Reactions** are `Like{content: "👍"|":name:", _misskey_reaction, tag:[Emoji]}`.
|
||
- One reaction per user per note; a plain like is `❤`.
|
||
- They are sent to the author **and to the reactor's followers**.
|
||
- **Actors:**
|
||
- `isCat`, `_misskey_summary`, `_misskey_followedMessage`;
|
||
- `_misskey_requireSigninToViewContents`;
|
||
- `_misskey_makeNotesFollowersOnlyBefore`/`HiddenBefore` (seconds; a negative value is relative to now);
|
||
- `vcard:bday`, `vcard:Address`, `backgroundUrl`.
|
||
- **Vanilla Misskey has no edits.**
|
||
- **Sharkey adds:**
|
||
- edits;
|
||
- a FEP-e232 quote `Link` tag (it deliberately leaves out `quote`);
|
||
- a `replies` collection;
|
||
- `hideOnlineStatus`, `noindex`, `enableRss`, `speakAsCat`.
|
||
- It sends contentless Likes only to Mastodon-like peers, so **we always receive Like+content**.
|
||
- **Iceshrimp.NET adds:**
|
||
- `EmojiReact`, several per user, and `:name@host:` for remote emoji;
|
||
- a FEP-7888 `context`;
|
||
- `htmlMfm: true` (FEP-c16b);
|
||
- every `QuoteRequest` is auto-accepted;
|
||
- `Bite`, `pronouns`.
|
||
- **CherryPick adds:** events on a plain Note (`startTime`/`endTime`), `deleteAt`, and federated chat
|
||
(`_misskey_talk: true`).
|
||
|
||
**Expects**
|
||
- **Inbound signatures:**
|
||
- draft-cavage over `(request-target) host date digest`;
|
||
- at most 300 s of skew;
|
||
- since 2026.10.0, the query string is included in `(request-target)`.
|
||
- **No RFC 9421 anywhere in the family.**
|
||
- **Activities:**
|
||
- An activity's `id` must be on the signer's host.
|
||
- Activities forwarded on behalf of someone else are refused. A group must `Announce`.
|
||
- Misskey answers 202 even when it drops something, so errors stay invisible.
|
||
- **Fetched documents:** request URL = final URL = `id`; ≤256 KiB; `activity+json` or `ld+json`.
|
||
- **Actor collections** must be on the actor's host.
|
||
- **Visibility:** Misskey recognises followers-only by the author's own `followers` URL, matched exactly. Otherwise:
|
||
- **A "specified" (direct) note with no resolvable recipients that Misskey fetches by URL is stored as public.**
|
||
Circle objects must therefore never be served to an unauthorised fetcher. They aren't: 404.
|
||
- **Groups:** vanilla Misskey drops `Announce{Create}`, so groups should `Announce` the Note itself. We send both.
|
||
- **Reactions:** must be `:name:` with no host, plus an Emoji tag, or they fall back to ❤.
|
||
- **Article/Page titles** are never shown in Misskey's web UI.
|
||
- **Iceshrimp.NET:**
|
||
- full JSON-LD expansion (see §1);
|
||
- `@graph`, `@reverse` and `@included` are refused;
|
||
- every actor must resolve through WebFinger;
|
||
- `preferredUsername` must be unique per domain. PrivaPub shares one name space across personas and groups, so this
|
||
holds.
|
||
|
||
**Gaps**
|
||
|
||
| Gap | P | Client surface |
|
||
|---|---|---|
|
||
| Emoji reactions in all three inbound forms: Like with `content`/`_misskey_reaction`, `EmojiReact`, and `Dislike`-as-un-like (Sharkey). Normalise `:n:`, `:n@host:` and `n@host`. Store per actor, emoji and activity, including reactions to remote posts. Undo by id. | P1 | `emoji_reactions` and `pleroma.emoji_reactions` `[{name,count,me,url,static_url}]` (Phanpy reads the first); `PUT/DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji` |
|
||
| Outbound reaction as `EmojiReact{content: ":name:", tag:[Emoji]}`. A plain Like stays a favourite. | P2 | same |
|
||
| Polls: per-option counts (fall back to `_misskey_votes`); `votersCount` null when absent; `Update{Question}` is a refresh; an inbound `Note{name, inReplyTo: question}` with no content is a vote, never a reply | P1 | `Status.poll` |
|
||
| Keep MFM source; let the sanitiser keep `<span class="mfm-*" data-mfm-*>` and `<ruby>` (FEP-c16b) | P2 | `text`, `content_type`; the rich client renders MFM from the source |
|
||
| Per-attachment `sensitive`; `isCat` and other actor extras; enforce `requireSignin…` and `makeNotes…Before` on our public pages | P2 | own `privapub.*` |
|
||
| Quotes: when we send one, send every key (`quote`, `_misskey_quote`, `quoteUrl`, `quoteUri`, a FEP-e232 tag, the `RE:` fallback) | P2 | — |
|
||
|
||
**Pasture evidence (2026-10-03, Misskey 2026.10.0, `tools/pasture/scenarios/misskey.sh`):** 35 checks pass:
|
||
- discovery and follows both ways;
|
||
- posts, CW and the MFM `source` kept;
|
||
- replies threading both ways;
|
||
- 👍 and 🎉 reactions both ways, and their withdrawal;
|
||
- a like counting as a reaction;
|
||
- legacy quotes both ways (`_misskey_quote` in, `renoteId` out);
|
||
- polls and votes both ways;
|
||
- `specified` notes as DMs both ways;
|
||
- images with alt text (`comment`) both ways;
|
||
- deletes both ways, unfollow and block;
|
||
- statistics.
|
||
|
||
A new Misskey 2026 starts with `federation: none`.
|
||
|
||
**Pasture evidence (2026-10-03, Sharkey 2025.4.7, `tools/pasture/scenarios/sharkey.sh`):** 40 checks pass, the whole
|
||
Misskey scenario under Sharkey's name and then what only Sharkey does:
|
||
- its edits arrive as edits, and ours reach it;
|
||
- its quote carries the FEP-e232 `Link` tag and is understood.
|
||
|
||
### Pleroma 2.10.2 and Akkoma 3.20.1
|
||
|
||
**Emits**
|
||
- **Notes:**
|
||
- `@context` with the instance's own `litepub-0.1.jsonld` URL (never fetch it) and `@language`;
|
||
- `source{content, mediaType: text/markdown}`;
|
||
- `context` and `conversation` holding the same value;
|
||
- `quoteUrl`/`quoteUri`.
|
||
- **Edit history:** `formerRepresentations`, an OrderedCollection of earlier versions.
|
||
- **Reactions:** `EmojiReact{content: ":name:", tag:[Emoji]}`, several per user; separate from Like.
|
||
- **Polls:** `votersCount` (Pleroma 2.10.1, Akkoma 3.20). A vote is a `Note{name, inReplyTo, to:[], cc:[owner]}`.
|
||
An open poll's end is in `closed`, and Akkoma sends no `endTime` (pasture, 3.20.1).
|
||
- **Pleroma only:**
|
||
- `ChatMessage`, sent only to actors with `capabilities.acceptsChatMessages`;
|
||
- `Listen{Audio}`;
|
||
- outgoing `Block` is on by default.
|
||
- **Akkoma only:**
|
||
- local-only posts are addressed to `<base>/#Public`, which is **not** public;
|
||
- FEP-2c59.
|
||
|
||
**Expects**
|
||
- **Pleroma's inbox guard answers 400 for unknown activity types:** `Move`, `QuoteRequest` and `Bite` are not on its
|
||
list (develop, 2026-09-30). Treat that 4xx as final.
|
||
- **Signatures (Akkoma):** `host` must be signed and match; signatures up to 2 h old and up to 40 min in the future.
|
||
- **Visibility is guessed from addresses:** a post is private only if an address in `to` contains `/followers` or its
|
||
`cc` is not empty; otherwise it is direct. Our followers-only posts therefore name `/groupies` in `cc` too.
|
||
- **Activity ids** must be at least 8 bytes.
|
||
- **ObjectAgePolicy** (default in both) delists anything older than 7 days, so `published` must be accurate.
|
||
- **Quotes:** Pleroma's InlineQuotePolicy rewrites incoming quotes into "RT: url" text. **Neither reads FEP-044f
|
||
`quote`**, so send `quoteUri` and `_misskey_quote` as well.
|
||
- **Edits:** a changed `name` is ignored on update.
|
||
|
||
**Gaps**
|
||
|
||
| Gap | P | Client surface |
|
||
|---|---|---|
|
||
| Language from `@context[].@language` | P1 | `Status.language` |
|
||
| `formerRepresentations` → our revisions | P2 | `/statuses/:id/history` |
|
||
| `context`/`conversation` threading; parents and quotes we could not fetch, kept as URIs | P2 | `pleroma.context`; `akkoma.in_reply_to_apid`, `akkoma.quote_apid` precedent |
|
||
| `ChatMessage` in as a direct message (also needed for Lemmy, Mbin and PieFed); advertise `acceptsChatMessages` only once it is answered | P1 (in), P3 (out) | `visibility: direct`, Conversations |
|
||
| `Listen`, `vcard:bday`, `backgroundUrl` | P3 | own |
|
||
|
||
**Pasture evidence (2026-10-03, Akkoma 3.20.1, `tools/pasture/scenarios/akkoma.sh`):** 44 checks pass, three runs in a
|
||
row. Akkoma publishes no image, so `tools/pasture/images/akkoma` installs its OTP release, pinned by checksum. Covered:
|
||
- discovery and follows both ways;
|
||
- posts, CW and followers-only posts;
|
||
- the `published` time kept;
|
||
- replies both ways and their notification;
|
||
- likes and boosts both ways with their undos;
|
||
- `EmojiReact` both ways and its withdrawal;
|
||
- DMs both ways and off public timelines;
|
||
- polls and votes both ways;
|
||
- quotes both ways (`quote_id` out of its API, `quoteUri` in ours);
|
||
- images with alt text both ways;
|
||
- edits with history, and deletes, both ways;
|
||
- unfollow, block and unblock;
|
||
- statistics.
|
||
|
||
It found two bugs, both fixed:
|
||
- **Followers-only posts arrived as DMs.** Akkoma, like Pleroma, calls a post private only if an address in `to` contains
|
||
`/followers` or its `cc` is not empty. Ours is `/groupies` and a post mentioning nobody had an empty `cc`. Followers-only
|
||
posts now name the followers collection in `cc` as well, which tells nobody anything new.
|
||
- **Open polls were shown as ended, and votes refused.** An open Akkoma poll carries its end in `closed`, with no
|
||
`endTime`. A `closed` in the future is now read as the end.
|
||
|
||
Seen along the way:
|
||
- Its Linkify never takes `@user@host.test` for a mention, whatever `validate_tld` says, so in the pasture Akkoma
|
||
addresses us with Pleroma's `to[]`. Real top-level domains are unaffected.
|
||
- It records our `Block` (`user_relationships`) but never reports a remote blocker as `blocked_by`.
|
||
- Its streamer crashes rendering a new DM conversation (`ConversationView`, a nil `last_status`); delivery is unaffected.
|
||
|
||
### Lemmy: 0.19.20 live (lemmy.ml); 1.0.0-beta.2 (2026-09-25) in beta since May
|
||
|
||
join-lemmy.org's federation page is out of date. Current Lemmy neither sends nor reads `stickied` or `commentsEnabled`
|
||
on a Page: pins live in `featured`, locks in `Lock`.
|
||
|
||
**Emits**
|
||
- **Group:**
|
||
- `summary` (sidebar HTML) and `source` (sidebar Markdown); a plain `description` in 1.0;
|
||
- `sensitive`; `attributedTo` → the moderators collection; `featured`; `postingRestrictedToMods`; `language[]`;
|
||
- 1.0 adds: `manuallyApprovesFollowers` for private communities, `discoverable: false` for unlisted ones, and the
|
||
community's post tags in `tag[]` as `CommunityPostTag` with colour slots `color01`–`color10`.
|
||
- **Page (post):**
|
||
- `name`, `content` and `source`;
|
||
- a link post is `attachment[0] = Link{href, mediaType}`, with `image` as the thumbnail;
|
||
- 1.0 image posts are `Image{url, name}`, where `name` is the alt text;
|
||
- `language{identifier, name}`, `audience`, `to: [community, Public]`;
|
||
- an automatic `#<community>` hashtag;
|
||
- 1.0 adds a Mention of the community and `context`.
|
||
- **Note (comment):** `distinguished`.
|
||
- **Votes:** `Like`/`Dislike` as `{actor, object, audience}` with no `to`/`cc`, and their `Undo`.
|
||
- **Moderation:**
|
||
- a `Delete` with a `summary` is a mod removal (1.0 adds `withReplies`); its `Undo` restores;
|
||
- `Lock`/`Undo{Lock}` (1.0 also on comments);
|
||
- a ban is `Block{target, removeData, summary, endTime}` (0.19 also sends `expires`);
|
||
- `Add`/`Remove` of featured posts and moderators;
|
||
- `Update{Group}` **by the moderator Person**;
|
||
- 1.0 adds `Warn` and `Resolve{Flag}`.
|
||
- **Delivery:** everything travels inside the community's `Announce`. For a new post Lemmy also sends a compatibility
|
||
`Announce(Page)` with a synthetic id; deduplicate it.
|
||
- **Private messages:** `ChatMessage` on 0.19; a single-recipient `Note` on 1.0.
|
||
- **Context:** 1.0's `context` collection is unpaged and **every comment's own context URL returns the whole post
|
||
thread**. Group threads by the root post, never by comparing `context` strings.
|
||
- **Feeds:** multi-communities are 1.0's `type: Feed` actors.
|
||
|
||
**Expects**
|
||
- **Fetched documents:** Content-Type exactly one of `activity+json`, `activity+json; charset=utf-8`, or `ld+json` with
|
||
the profile; and `id` equals the URL fetched.
|
||
- **Addressing:**
|
||
- Create, Update, Lock, Delete and Block **must carry both `to` and `cc` arrays**.
|
||
- A post's community is the first Group found in `to` ∪ `cc`.
|
||
- In a public community, the object, the activity **and the Announce** all include Public.
|
||
- **Accepted types:** Page, Article, Note, Video and Event become posts. **`Question` is dropped.**
|
||
- **Anti-spam:** activities for a community are accepted only if a local user follows it.
|
||
- **Actors:** a Create, vote, moderation action or Flag must come from a Person, Service or Organization. **A Group or
|
||
Application actor fails to parse**, so moderation comes from the moderator Person. Our Flags, sent by an
|
||
Application instance actor, probably fail *(unconfirmed)*.
|
||
- **Flags:** exactly one `to` (community or site); `object` a URL or an array; the reason in `summary` or `content`.
|
||
- **Moderation trust:** an action is trusted when it is on the community's or the object's host (FEP-fe34), or when its
|
||
actor is in the moderators list Lemmy fetched.
|
||
- **1.0 hides a local user's post or comment in a remote community until that community Announces it back.** A
|
||
community we host must therefore announce to the author's own instance too.
|
||
- **Refused:** Lemmy does not accept an incoming `Announce(Page)`.
|
||
|
||
**Gaps**
|
||
|
||
| Gap | P | Client surface |
|
||
|---|---|---|
|
||
| `Dislike` and its `Undo`: a vote ledger per object (actor, ±1, activity, relaying group, time) | P1 | `favourites_count` = upvotes; own `privapub.vote{score, up, down, mine}`, `POST …/vote` |
|
||
| Announces of activities other than Create (votes, moderation): trust the inner activity when the object's own group signed the Announce; refetching every vote does not scale. Keep the origin refetch for Create and Update. | P1 | — |
|
||
| Moderation state: removals (reason, by, at, cascade), locks, bans with `endTime`/`removeData`, featured, moderators, `Update{Group}` by a moderator | P1 | removed posts hidden plus `privapub.removed`; `privapub.locked` (replying answers 422); pins as `pinned=true` |
|
||
| Link posts: keep `Link.href`, the thumbnail `image` and alt text; build the card (Lemmy sends no title or description for the link) | P1 | `Status.card` |
|
||
| `ChatMessage` in and out (out only to Lemmy < 1.0 and Mbin; `Note` to everyone else) | P1 | `visibility: direct` |
|
||
| Outbound shape for Lemmy: both `to` and `cc`; the community in `to`; Public in the object, Create and Announce; votes and comments sent to the community inbox | P1 | — |
|
||
| Communities we host: pick `Announce(object)` per peer by NodeInfo (as PieFed does). Announcing to every follower instance, the author's included, is done and needed (pasture evidence below) | P1 | — |
|
||
| Flags from a `Service`-typed reporter actor with `to: [community]`; the reporter stays anonymous | P2 | — |
|
||
| `Warn` → `moderation_warning` notification; `Resolve{Flag}` | P2 | AccountWarning |
|
||
| Remote communities: `description`, `language[]`, private (`locked`), `discoverable`; post tags | P2 | `Account.locked`, own `privapub.flairs[]` |
|
||
| Serve our communities' collections the way Lemmy reads them: inline outbox of `Announce{Create{Page}}`, inline featured Pages, inline moderators. Lemmy does not page. | P2 | — |
|
||
| `Feed` actors | P2 | group-like account |
|
||
| Read 1.0 `context`, grouped by root post; cross-post detection by URL | P3 | — |
|
||
|
||
**Pasture evidence (2026-10-03, Lemmy 1.0.0-beta.2, `tools/pasture/scenarios/lemmy.sh`):** 20 checks pass and 3 are
|
||
expected failures:
|
||
- communities both ways: Lemmy follows ours and alice follows Lemmy's, each Accept arriving;
|
||
- a Lemmy thread in our community arrives with its title, and our titled community post reaches Lemmy;
|
||
- the Lemmy community's Announce brings its thread to alice's home;
|
||
- alice's post mentioning a Lemmy community lands in it, titled from its first line (Lemmy repeats that line in the
|
||
body);
|
||
- comments both ways, and alice's like counted as an upvote;
|
||
- private messages both ways: 1.0 takes our single-recipient direct `Note`, and sends its own as `Note`s;
|
||
- statistics.
|
||
|
||
What it showed:
|
||
- **1.0 keeps its user's thread in a remote community `federation_pending` until the community announces it back**, and
|
||
clears the flag *before* answering that echo 400 (`Object is not remote`). Without the echo the thread stays pending,
|
||
so `GroupDistributor` sends the `Announce{Create}` to the author's own server on purpose.
|
||
- **Every bare `Announce{object}` is answered 400** (`Failed to parse object`: Lemmy dereferences it expecting an
|
||
activity), as Lemmy answers the compatibility `Announce(Page)` it sends itself. Both 400s show up as dead deliveries
|
||
in the statistics.
|
||
- **Votes travel only to the community**, which relays them as `Announce{Like}` and `Announce{Dislike}`. They are
|
||
dropped as `unsupported` until P7 (expected failures), as is a moderator's removal.
|
||
- Lemmy logs no refused activity at `warn`; the reason is in the 400's body, which our delivery does not keep. The
|
||
scenario's API notes: `sort` values are lowercase (`new`), private messages and mentions are in
|
||
`account/notification/list`, and `resolve_object` takes both `!community@host` and `@user@host`.
|
||
- **1.0 sends nothing it queued for a server before it started that server's send worker.** A worker starts, up to a
|
||
minute after the server is first seen, at the newest activity and skips everything older. On a clean pasture its
|
||
first Follow of our community was lost, so the scenario waits for the worker (`federation_queue_state`). On the
|
||
public network the same applies to the first thing a Lemmy sends to a PrivaPub it has just discovered.
|
||
- 0.19 cannot join the pasture: its rustls trusts only its bundled roots, never Caddy's CA.
|
||
|
||
### PieFed 1.7.17 and Mbin 1.10.1
|
||
|
||
- **PieFed sends Lemmy's set plus:**
|
||
- posts: `commentsEnabled`, `stickied`, `nsfl`, `genAI`, `searchableBy`, `canQuote`;
|
||
- galleries of several Documents;
|
||
- **polls in communities** (`Question` with `votersCount`), and Mobilizon-style `Event`s;
|
||
- flairs in two dialects;
|
||
- comments: `repliesEnabled` (comment lock), `answer`, plus custom `ChooseAnswer` and `PollVote` activities;
|
||
- `Move{object: post}` between communities;
|
||
- `Feed` actors;
|
||
- emoji Likes and `EmojiReact` count as upvotes.
|
||
- **How PieFed formats what it sends us:**
|
||
- It **chooses the format by our NodeInfo software name**, so keep that accurate.
|
||
- Software it lists as microblogs gets a bare `Announce(URL)`.
|
||
- A poll vote is recognised only when it is a `Note{name, inReplyTo}` **without `published`**.
|
||
- Batched Announces (FEP-1a11) go only to PieFed and Pylova.
|
||
- **Mbin:**
|
||
- Thread `summary` is "short title + tags", so it is **not a CW**.
|
||
- On link threads `source` is a **plain URL string**.
|
||
- `commentsEnabled` and `stickied` are sent.
|
||
- A Person's `Announce` counts as an upvote; Mbin reads the `likes`/`dislikes`/`shares` counts we publish.
|
||
- Private messages are `ChatMessage`.
|
||
- The magazine outbox is empty.
|
||
- Mbin also auto-ingests Mastodon posts by hashtag and Announces them.
|
||
- **Gaps:**
|
||
- **P1:** Lemmy's P1 set, W2, and tolerating `source` as a string.
|
||
- **P2:** galleries; community polls (vote without `published`); post `Move`; `repliesEnabled`; `nsfl`; flairs;
|
||
publish `likes`/`shares` totals.
|
||
- **P3:** `genAI`; accepted answers; batched Announces.
|
||
|
||
### NodeBB 4.16, Discourse, Friendica
|
||
|
||
- **NodeBB:**
|
||
- A topic's first post is an `Article` with `name`, **`summary` = an excerpt** and `preview`; replies are Notes.
|
||
- Categories are `Group`s **without `followers`**.
|
||
- `context` is a paged collection with an **ETag digest**; NodeBB refetches with `If-None-Match`.
|
||
- Since 4.15, an Announce of anything but a Create or a plain object is accepted only from Group actors.
|
||
- It sends `Move`/`Remove` of a whole context (FEP-f15d) and `Add{post → context}` (FEP-11dd).
|
||
- **Discourse** (plugin, semi-dormant): categories and tags are Groups. "Full Topic" mode makes the topic an
|
||
OrderedCollection used as `context`.
|
||
- **Friendica** (2026.05-1):
|
||
- Group accounts relay with `Announce(object)`.
|
||
- Titled posts are `Page`/`Article`; it sends `Dislike`.
|
||
- `instrument{Service}` names the software.
|
||
- It sends **`Follow` with a post as the object**, meaning "include me in this thread". Answer that with `Reject` or
|
||
ignore it, without an error.
|
||
- **Gaps:**
|
||
- **P1:** W2 for NodeBB Articles (`privapub.excerpt`).
|
||
- **P2:** Groups without `followers`; Announces from an Application; context Move/Remove; paged `context` with ETag;
|
||
Friendica's thread-Follow.
|
||
|
||
### PeerTube 8.3.1 (2026-09-28)
|
||
|
||
**Emits**
|
||
|
||
The account sends `Create{Video}`; the channel (a Group) sends `Announce{Video}`, so deduplicate.
|
||
|
||
| Part of the Video | What it holds |
|
||
|---|---|
|
||
| Attribution | `attributedTo: [Person, Group]` (both, possibly bare URLs); `audience` = the channel |
|
||
| Description | Markdown in `content` with `mediaType: text/markdown`; `summary` is the CW (since 7.2) |
|
||
| `url[]` | A `text/html` watch page; per-resolution mp4 Links (`height`, `width`, `fps`, `size`, ffprobe codec types); HLS `application/x-mpegURL` (since 6.3 audio and video can be separate, with "0" as the audio-only resolution); torrent and magnet; a metadata JSON |
|
||
| Images | `icon[]`: thumbnails up to 1920 px; `preview`: storyboards |
|
||
| Captions | `subtitleLanguage[]` with VTT and HLS URLs |
|
||
| Chapters | `hasParts` |
|
||
| Playback and metadata | `duration` (ISO 8601), `views`, `state`, `isLiveBroadcast`, `permanentLive`, `latencyMode`, `commentsPolicy`, `downloadEnabled`, `category`, `licence`, `language`, `support`, `uuid`, `embedUrl`, `originallyPublishedAt`, `schedules`, `aspectRatio` |
|
||
| Sensitivity | `SensitiveTag` |
|
||
|
||
- **Value meanings:**
|
||
- **Live now** means `isLiveBroadcast && state == 1`.
|
||
- `commentsPolicy`: 1 open, 2 closed, 3 needs approval.
|
||
- **Other activities:**
|
||
- Comments are Markdown Notes.
|
||
- `View` comes from the server's Application actor.
|
||
- `Dislike`; `ApproveReply` (FEP-5624, since 6.2); `CacheFile` (mirrors); playlists.
|
||
|
||
**Expects**
|
||
- **Replies** must:
|
||
- be Public;
|
||
- have non-empty `content`, a valid `url` and `published`;
|
||
- have an `id` on the actor's host;
|
||
- have an `inReplyTo` that resolves to the video or one of its comments.
|
||
- `commentsPolicy` 2 rejects replies; 3 holds them until approved.
|
||
- PeerTube signs its fetches.
|
||
- It drops followers that have been unreachable for about 7 days (8.2).
|
||
|
||
**What Mastodon does with it:** `<h2>name</h2>` + summary + link. The description is dropped and there is no
|
||
attachment. The player is a card whose iframe loads from the remote host, which our proxy rule forbids.
|
||
|
||
**Gaps**
|
||
|
||
| Gap | P | Client surface |
|
||
|---|---|---|
|
||
| Store the whole Video: variants, thumbnails, storyboards, captions, chapters, duration, live state, views, `commentsPolicy`, licence, category, language, support, channel | P1 | own `privapub.video` |
|
||
| Play through the proxy: a `MediaAttachment{type: video}` pointing at a proxied muxed mp4 (a `web-video` file, or a fragmented file whose codec types include both audio and video); `preview_url` = a ~560 px `icon`; `meta.original` with width, height, `frame_rate`, `duration` | P1 | `media_attachments` |
|
||
| The proxy answers **Range** requests and rewrites HLS playlists and caption URLs to proxied ones. A 720p file is about 0.9 GB, so stream it; never buffer. | P1 | — |
|
||
| A video card made from the object, **without** a remote iframe | P1 | `Status.card{type: video}` |
|
||
| Reply rules: closed when `commentsPolicy` is 2; replies sent Public with a `url`; `ApproveReply` shows our reply as pending | P1 / P2 | own |
|
||
| Dislike counts; live state through `Update`; chapters and captions | P2 | own |
|
||
| Optionally, `View` sent from the instance actor (so it never names a persona) | P3 | — |
|
||
|
||
### Loops (1.0.0-beta.14) and Pixelfed (0.14.4)
|
||
|
||
- **Loops:**
|
||
- A video is a Note with one mp4 `Document` whose `url` is a string. **The poster is in the object's `preview`.**
|
||
Videos are vertical.
|
||
- Its interactionPolicy follows GoToSocial's model.
|
||
- It sends `QuoteRequest` and `FeatureRequest`.
|
||
- A top-level post it accepts must be a Note with an mp4 attachment, from an instance its admin allowlisted, **≤ 100 MB,
|
||
checked with a HEAD request**. Our media must answer HEAD.
|
||
- **Pixelfed:**
|
||
- Posts are a Note with attachments.
|
||
- It also sends `location: Place{name, latitude, longitude, country}`, `commentsEnabled`, `capabilities`, and
|
||
`canQuote` (0.14).
|
||
- Stories are `Add{Story}` with a bearcap only Pixelfed understands.
|
||
- Pixelfed 0.14 does FEP-044f and FEP-8fcf.
|
||
- **What Pixelfed accepts:**
|
||
- Only `Note`s, and **a top-level post must have media**.
|
||
- **Every** attachment must be a Document or Image with a string `url` and a `mediaType` in the instance's list. The
|
||
default list is **jpeg, png and gif only**, and a single webp or avif attachment rejects the whole post.
|
||
- **Gaps:**
|
||
- **P1 outbound:** keep JPEG/PNG renditions with an explicit `mediaType`.
|
||
- **P1 inbound:** Loops' `preview` poster.
|
||
- **P2:** Pixelfed `location` → own `privapub.place`, display only and never re-federated; `commentsEnabled: false`
|
||
disables replies.
|
||
- **P3:** ignore `Add{Story}` without an error; answer `FeatureRequest` with `Reject`.
|
||
|
||
### Long-form: WordPress plugin 9.3.1, Ghost 6, WriteFreely 0.17.2
|
||
|
||
FEP-b2b8 (draft) describes the shape: plain-text `name`, a `summary` teaser (≤500), full HTML `content`, `image`, and
|
||
a `preview` Note fallback.
|
||
|
||
- **WordPress:**
|
||
- Object type: an Article for a titled post, a Page for a page, otherwise a Note.
|
||
- Fields: `image` (the featured image), `preview`, `interactionPolicy.canQuote`.
|
||
- A CW is `sensitive` + `summary` + `dcterms:subject`.
|
||
- `id` is `?p=123`, different from `url`.
|
||
- The blog actor is a Group with `attributionDomains`.
|
||
- It sends an `Update` on every save, and **signs with RFC 9421 first** (9.3.0), falling back to draft-cavage after
|
||
any 4xx.
|
||
- It drops followers-only replies.
|
||
- **Ghost 6** (its ActivityPub service is separate, built on Fedify):
|
||
- Article with `image` as a bare string and `preview`; members-only parts removed.
|
||
- It refetches every object signed, **never applies remote Updates**, and accepts only Note and Article.
|
||
- Public is addressed as `as:Public`.
|
||
- **WriteFreely:** Article when the body has a paragraph break. **`preview` reuses the Article's id**, so never store
|
||
it as its own post. It has no comments.
|
||
- **Gaps:**
|
||
|
||
| Gap | P | Client surface |
|
||
|---|---|---|
|
||
| An Article shown in the Mastodon API: `content` = name + teaser (`summary`, else `preview.content`) + a link to `url`; a card made from the object (title, description, `image` then `icon`, author, provider, date) | P1 | `Status.content`, `Status.card` |
|
||
| The full sanitised HTML kept for a reader view | P1 | own `privapub.article{title, html, cover, excerpt}` |
|
||
| Body images duplicated in `attachment` removed; `attributedTo` arrays resolved to the Person | P2 | — |
|
||
|
||
### Events: Mobilizon 5.2.4, Gancio 1.28, Friendica/Hubzilla/Forte
|
||
|
||
FEP-8a8e (draft) is the common reference.
|
||
|
||
- **Mobilizon:**
|
||
- Times and status: `startTime`, `endTime`, `timezone`, `status`/`ical:status`, `isOnline`, `draft`.
|
||
- Place: `location: Place{address: PostalAddress, latitude, longitude}`.
|
||
- Participation: `joinMode`, `participantCount`, `maximumAttendeeCapacity`, `remainingAttendeeCapacity`,
|
||
`anonymousParticipationEnabled`.
|
||
- Comments: `repliesModerationOption`, `commentsEnabled`.
|
||
- Other: `category`, `contacts`.
|
||
- Attachments: the online link `Link{name: Website}`; `PropertyValue`s under `mz:` keys; a banner `Document`.
|
||
- The event is attributed to the Group.
|
||
- **RSVP:** `Join{object: event}` with a stable id that can be fetched; Mobilizon answers `Accept` or `Reject`;
|
||
`Leave`.
|
||
- **Gancio:** a single Application actor; `location` is an **array** of `VirtualLocation` and `Place`; no RSVP.
|
||
- **Friendica, Hubzilla:** RSVP with `Accept`/`Reject`/`TentativeAccept`. Hubzilla creates events as `Invite{Event}`,
|
||
with HTML in `location.content`, and `-00:00` for floating times.
|
||
- **FEP-8a8e:** a server that does not handle joins answers `Join` with `Ignore`.
|
||
- **Gaps:**
|
||
|
||
| Gap | P | Client surface |
|
||
|---|---|---|
|
||
| Store times, time zone and place in every one of those shapes; RSVP routing (W6); `Invite{Event}`; answer `Join` with `Ignore` until RSVP exists | P1 | `content` = title + "date (zone) · place" + link; card with the banner |
|
||
| Capacity, join mode, online link, status, category | P2 | own `privapub.event` |
|
||
| RSVP out (`Join`/`Leave`, stable ids), from the persona the user chose | P2 | own `…/rsvp` |
|
||
|
||
### Audio: Funkwhale 2.0.11, Castopod 1.15.5, Owncast 0.3.0
|
||
|
||
- **Funkwhale:**
|
||
- `Create{Audio}` whose `url` is an array: a page link plus the stream, with `bitrate` and `size`.
|
||
- Also `duration`, `position`, `disc`, `album`, `license` and `image`; `summary` is a hashtag line (W2).
|
||
- `Listen{Track}`.
|
||
- **Castopod:** an episode arrives as a link-only Note; the player comes from OpenGraph/oEmbed. Fetching the episode
|
||
with the podcast type gives a `PodcastEpisode` with transcript and chapters.
|
||
- **Owncast:** a `Service` actor; "go live" is a Note with a thumbnail.
|
||
- **Gaps:**
|
||
- **P1:** pick the audio Link out of `url`, sniff its type, apply W2. Surfaces as `MediaAttachment{type: audio}`
|
||
with `meta.original.duration` and the cover as `preview_url`.
|
||
- **P2:** duration, cover, album, position and licence (own `privapub.audio`); the Castopod transcript and chapters.
|
||
- **P3:** `Listen` history; Owncast live state.
|
||
|
||
### Books: BookWyrm 0.9.3, NeoDB 0.19.4
|
||
|
||
- **BookWyrm:**
|
||
- **To non-BookWyrm servers** it sends a review as an `Article` named `Review of "Title" (★★★★): …`, a comment or
|
||
quotation as a Note with the citation appended, and the cover as a `Document`.
|
||
- The fields `inReplyToBook`, `rating` and `quote` go only to BookWyrm servers.
|
||
- **NeoDB:** `relatedWith[]` carries the item, the rating (out of 10), the review and the shelf; `tag[]` holds catalogue
|
||
items (`{type: "Movie", href, name, image}`).
|
||
- **Gaps:**
|
||
- **P1:** keep the Article `name`, `tag` entries that are not Mention or Hashtag, and covers that have no blurhash.
|
||
- **P2:** keep `relatedWith`, `rating` and `inReplyToBook` raw; a card for the item; own
|
||
`privapub.review{item, rating, scale}`.
|
||
|
||
### Threads, Flipboard, Bluesky via Bridgy Fed
|
||
|
||
- **Threads:**
|
||
- **Unsigned GETs answer 404**, so fetch it signed, as our instance actor.
|
||
- Quotes arrive as `_misskey_quote` + a FEP-e232 tag + an `RE:` fallback.
|
||
- Polls are not federated.
|
||
- Threads users must be 18 or over, opt in, and live outside the EU.
|
||
- Threads blocks servers that ignore deletes or have no privacy policy.
|
||
- **P1:** process `Delete` promptly; publish a privacy policy and a minimum age.
|
||
- **Flipboard:**
|
||
- Each flip is a link-only Note: headline, a link with `utm_*` parameters, and a Mention of the magazine.
|
||
- Magazines are Groups that `Announce` bare URIs.
|
||
- **P1:** fetch objects that arrive as bare-URI Announces (we do).
|
||
- **P2:** a card, or the post reads as a bare headline; deduplicate by canonical URL with `utm_*` stripped.
|
||
- **Bridgy Fed:**
|
||
- Actors are `https://bsky.brid.gy/ap/did:plc:…`, with handles like `@x.bsky.social@bsky.brid.gy` (**dots in the
|
||
user part**). `alsoKnownAs` holds `did:` values; there is no `published` and the outbox is empty.
|
||
- Posts: a `url` array with an `at://` canonical link (W1); **media without `mediaType`** (W4); link embeds flattened
|
||
into `content`; video thumbnails in `image`; long-form as Article.
|
||
- **Bridging a persona:**
|
||
- opt-in, by following the bot;
|
||
- the persona needs an `icon` and must be at least 7 days old;
|
||
- only public posts are bridged;
|
||
- **opting out takes a `Block` sent to the bot**, which we never send today.
|
||
- **Privacy note:** two personas created on the same day share the same day-truncated `published`. That is weak,
|
||
but a link.
|
||
- **P2:** `Account.created_at` fallback; the Bridgy opt-out Block (see §6).
|
||
|
||
## 4. What the rich client needs from the server
|
||
|
||
The rule: **never drop what arrived; keep it typed where we understand it and raw where we don't.** The Mastodon API
|
||
keeps working for existing apps. Everything it cannot express goes under a `privapub` object on the same entities,
|
||
following the precedent of `pleroma.*` and `akkoma.*`, plus a few endpoints of our own.
|
||
|
||
### 4.1 One post, many kinds
|
||
|
||
`Post` gains a `Kind` and one typed payload per kind.
|
||
|
||
| Kind | From | Typed payload | Mastodon API view | `privapub.*` |
|
||
|---|---|---|---|---|
|
||
| note | everyone | — | as today | `source` (Markdown/MFM/BBCode), per-attachment `sensitive` |
|
||
| article | WordPress, Ghost, WriteFreely, NodeBB, BookWyrm, Bridgy | title, excerpt, cover, full HTML, `preview` | content = title + excerpt + link; card from the object | `article` (reader view) |
|
||
| video | PeerTube, Loops, Bridgy, Owncast | variants, HLS, poster, storyboards, captions, chapters, duration, live state, licence, channel | proxied `video` attachment + card | `video` |
|
||
| audio | Funkwhale, Castopod | stream variants, cover, duration, album, position, licence, transcript, chapters | `audio` attachment | `audio` |
|
||
| event | Mobilizon, Gancio, PieFed, Friendica, Hubzilla, CherryPick | start, end, zone, place, online link, join mode, capacity, status | text + card | `event` (+ RSVP) |
|
||
| poll | Mastodon, Misskey, Pleroma, GoToSocial, PieFed | options, counts, multiple, end, closed, voters | `poll` | — |
|
||
| link | Lemmy, PieFed, Mbin, Flipboard, Mastodon 4.7 | href, thumbnail, alt, author | `card` | `link` |
|
||
| review | BookWyrm, NeoDB | item, rating, scale, shelf | text + card | `review` |
|
||
| page / thread | Lemmy, PieFed, Mbin, NodeBB | title, community, flair, lock, removal, score | content + `card` for links | `thread{community, flairs, locked, removed, score, distinguished}` |
|
||
|
||
These cut across every kind:
|
||
|
||
| Feature | Mastodon API | `privapub.*` |
|
||
|---|---|---|
|
||
| Quote | `quote{state, quoted_status}` | the URI when it could not be fetched |
|
||
| Emoji | `emojis` | — |
|
||
| Reactions | `emoji_reactions` (and `pleroma.emoji_reactions`) | — |
|
||
| Votes | — | `vote{score, up, down, mine}` |
|
||
| Interaction policy | GoToSocial-style `interaction_policy` | — |
|
||
| Edit history | `edited_at`, `/history` | — |
|
||
|
||
### 4.2 Linking things together
|
||
|
||
Every reference is kept as a URI even when the target could not be fetched: the parent, the quoted post, the
|
||
community, the channel, the book, the original `url`. The client can then link out where it cannot embed. This
|
||
follows the `akkoma.in_reply_to_apid` / `akkoma.quote_apid` precedent.
|
||
|
||
A link opens `url` (W7), never `id`. Media and thumbnails only ever go through the proxy, so the client never contacts
|
||
a remote host.
|
||
|
||
### 4.3 Details view ("nerd stats")
|
||
|
||
`GET /api/privapub/v1/statuses/:id/provenance` (and the same for accounts):
|
||
|
||
| Group | Fields |
|
||
|---|---|
|
||
| Raw | The object exactly as received, with its hash. Every later refetch and `Update`, with timestamps. The `@context` as sent (the namespaces show `toot`, `misskey`, `litepub`, `lemmy`, `pt`, `mz`, `gts`, `fedibird`, …). |
|
||
| Path in | How it arrived: direct Create, inbox forward, Announce (and by whom: community, channel, magazine, relay), backfill, or fetch on demand. Delivered to the personal or the shared inbox. |
|
||
| Trust | Signature scheme: draft-cavage (algorithm string, signed headers, query signed or not), RFC 9421, FEP-8b32 proof, or verified by refetch from origin. Key id and key type. LD signature present but ignored. |
|
||
| Time | `published`, `updated`, received, and their gaps (backdated posts, clock skew). |
|
||
| Extensions | Detected from properties: 044f `quote`, `interactionPolicy`, 7888 `context`, 8b32 `proof`, 521a `assertionMethod`, 8967 Link attachment, c16b `htmlMfm`, `_misskey_*`, `searchableBy`, `formerRepresentations`, FEP-1b12 `audience`. |
|
||
| Origin | Software and version from NodeInfo, with a family. This is for display only: FEP-0151 says the names are opaque, so never branch on them except where a peer does the same to us (PieFed). |
|
||
| Media | Variants with codec, fps, size and bitrate; infohashes, magnets and mirrors; licence. |
|
||
| Counts | Remote likes, boosts, views, downloads and dislikes as last seen, with the time. |
|
||
| Interactions | Policy as received, defaults applied, approval state, authorization URIs. |
|
||
| Moderation | Removals, locks and bans as received from a community, with reasons. |
|
||
|
||
`GET /api/privapub/v1/instances/:host` returns cached NodeInfo (`metadata` extras: Misskey `themeColor`/`maxNoteTextLength`,
|
||
Pleroma `features[]`/`federation.mrf_policies`), the instance API's icon and description, the delivery health our
|
||
circuit breaker sees, and which signature scheme worked.
|
||
|
||
`software.name` → family, for display:
|
||
|
||
| Family | `software.name` values |
|
||
|---|---|
|
||
| Mastodon | mastodon (glitch-soc shows `+glitch` in the version), hometown, fedibird, kmyblue |
|
||
| Misskey | misskey, sharkey, cherrypick, iceshrimp, firefish, foundkey, catodon |
|
||
| Pleroma | pleroma, akkoma |
|
||
| Microblog | gotosocial, takahe, hollo, mitra, snac, smithereen, ktistec, wafrn, bonfire, friendica, hubzilla, streams/forte |
|
||
| Threadiverse | lemmy, piefed, mbin, kbin, lotide, nodebb, discourse |
|
||
| Media | peertube, pixelfed, loops, funkwhale, owncast, vernissage, castopod |
|
||
| Publishing | wordpress, ghost, writefreely, plume |
|
||
| Events | mobilizon, gancio |
|
||
| Other | bookwyrm, neodb, forgejo |
|
||
| Bridges and relays | bridgy-fed, activityrelay |
|
||
|
||
## 5. Signatures, identity and transport
|
||
|
||
| Topic | State on 2026-10-01 | PrivaPub | P |
|
||
|---|---|---|---|
|
||
| RFC 9421 inbound | Mastodon accepts since 4.5. WordPress and Fedify sign with it first. GoToSocial, the Misskey family, Akkoma, Pleroma and Bridgy do not. | Verify RSA and Ed25519; `content-digest` (RFC 9530); one signature; `created` and `keyid` | P2 |
|
||
| RFC 9421 outbound | Mastodon 4.7 double-knocks | draft-cavage first; RFC 9421 after a 401; remember per host | P2 |
|
||
| 400 vs 401 | A 400 or 401 makes Mastodon 4.7 and WordPress retry with the other scheme | 401 only for signature failures (we do this); 400 only for bodies that are really malformed | P1 (keep) |
|
||
| Temporary key failure | Mastodon answers 503 | We should answer 503 too, and treat a 503 as a retry in delivery | P2 |
|
||
| Keys | `publicKey` can be an array (Mastodon 4.6). FEP-521a `assertionMethod` Multikey is FINAL (Ed25519 `z6Mk…`). GoToSocial key ids have no `#` and point at a stub. | Read all of these | P2 |
|
||
| Integrity proofs | FEP-8b32 `eddsa-jcs-2022`: JCS, no JSON-LD. Sent by Mitra, Streams, Hubzilla, Fedify and others; Mastodon verifies them from 4.7 | Verify, so relayed or forwarded objects need no refetch | P2 |
|
||
| LD signatures | Mastodon still sends `RsaSignature2017` | Ignore, and refetch from origin (we do) | — |
|
||
| Query string | GoToSocial, Akkoma 3.20 and Misskey 2026.10 sign it; GoToSocial retries without it | Verify both ways | P1 |
|
||
| `hs2019` | The algorithm comes from the key; some senders hash with SHA-512 | Try rsa-sha256, then sha512 | P2 |
|
||
| Move (FEP-7628, FINAL 2026-08-26) | See Mastodon | Inbound P1; outbound per persona P3 (never link personas) | P1 / P3 |
|
||
| Followers sync (FEP-8fcf) | Mastodon, Pixelfed, Fedify and WordPress | Send and honour `Collection-Synchronization`. It protects followers-only posts. | P2 |
|
||
| Instance actor discovery | FEP-d556 (FINAL), FEP-2677 | Publish both | P3 |
|
||
| Relays (FEP-ae0c, FINAL) | Mastodon-style relays forward LD-signed Creates; LitePub-style relays Announce. GoToSocial 0.22 subscribes to relays. | Client for both styles; refetch or check an integrity proof. This is how a small server sees beyond its follows. | P2 |
|
||
| FASP | Mastodon 4.4+, behind a flag. Its data sharing pushes content to a third party. | Do not join the data sharing; maybe consume search and trends | P3 |
|
||
| Search consent | `indexable` (missing = false), `discoverable`, `searchableBy` (FEP-268d, which takes precedence) | Honour all three; emit explicit `false` per persona | P2 |
|
||
|
||
## 6. Choices the privacy design has to make
|
||
|
||
These change what PrivaPub reveals, so they are the owner's calls, not implementation details. **All six were decided on
|
||
2026-10-01; the decisions are in `docs/ROADMAP.md`, "Owner decisions on what PrivaPub reveals".** In short:
|
||
1. pages are fetched by the server for public posts;
|
||
2. blocks federate;
|
||
3. website authorship stays off;
|
||
4. PeerTube views are never sent;
|
||
5. Bluesky bridging is per persona, with persona dates randomised;
|
||
6. a one-time notice before the first reaction or vote.
|
||
|
||
The options as they were laid out:
|
||
|
||
1. **Link previews.**
|
||
- Building a card from the object, or from FEP-8967 `preview` data, fetches nothing; we do that.
|
||
- Fetching the linked page tells that site our server read the link. Cache per URL across personas, so no fetch
|
||
ties a page to one persona; add jitter.
|
||
- Proposed admin switch: *off / from the object only / fetch*.
|
||
2. **Outbound Block.**
|
||
- Blocks are never sent today.
|
||
- Bridgy Fed's opt-out *requires* one, and Mastodon, GoToSocial and Misskey federate blocks as a matter of course.
|
||
- Proposal: never by default; an explicit per-persona "tell their server" option.
|
||
3. **`attributionDomains` / `fediverse:creator`.** It ties a persona to a website. Opt-in per persona only.
|
||
4. **PeerTube `View`.** Counting a view tells the origin a video was watched. If ever, send it from the instance actor.
|
||
5. **Bridging to Bluesky.** It is per persona; each persona needs its own avatar and 7 days of age. Same-day personas
|
||
share a `published` day.
|
||
6. **Emoji reactions and votes** are public by nature on every platform that has them. The client should say so
|
||
before the first one.
|
||
|
||
## Sources
|
||
|
||
The full source lists, with versions and dates, were gathered by five research passes on 2026-10-01. The primary
|
||
ones:
|
||
|
||
| Area | Sources |
|
||
|---|---|
|
||
| Mastodon | `CHANGELOG.md` (4.3.0–4.7.2); docs.joinmastodon.org (`spec/activitypub`, `spec/security`, `spec/webfinger`, `client/quotes`, `client/collections`, entities); the `main` source (`note_serializer.rb`, `activity/{create,update,accept,delete,quote_request,move,like,flag}.rb`, `status_parser.rb`, `media_attachment_parser.rb`, `fetch_link_card_service.rb`, `verify_quote_service.rb`, `fetch_all_replies_service.rb`, `signed_request.rb`); developer posts for 4.5, 4.6 and 4.7; GHSA-vwhj, GHSA-rwcw, GHSA-vg36; issue #39997 (spam, July 2026) |
|
||
| GoToSocial | Codeberg releases 0.18.0–0.22.1; `docs/federation/*` (`interaction_controls`, `posts`, `actors`, `http_signatures`, `access_control`); `internal/typeutils`, `internal/ap`, `internal/federation`, `internal/transport`; issues #4939, #1894, #4994, #4849, #4677 |
|
||
| Misskey family | misskey-dev/misskey develop `ed9654b` (`ApRendererService`, `ApInboxService`, `ApNoteService`, `ApAudienceService`, `ReactionService`, `check-against-url`); PR #16250; Sharkey develop `effcecb`; Iceshrimp.NET dev `926fcde` (`FEDERATION.md`, `LdHelpers.cs`, `HttpSignature.cs`, `NoteRenderer.cs`); CherryPick develop |
|
||
| Pleroma, Akkoma | Pleroma develop `cfeca7f` (`transmogrifier.ex`, `inbox_guard_plug.ex`, `ap_extensions.md`); Akkoma develop (`FEDERATION.md`, `CHANGELOG.md`, `nodeinfo_extensions.md`) |
|
||
| Lemmy | LemmyNet/lemmy `main` `f1476db` and tag 0.19.20 (`crates/apub/apub/assets/*` fixtures, `objects/src/protocol/*`, `activities/src/protocol/*`, the pending-post migration); PRs #5856, #6152, #6401, #6409, #6466; issues #6281, #6343, #6346; activitypub-federation-rust `fetch/mod.rs` |
|
||
| Other threadiverse | PieFed `41cccb7` (`FEDERATION.md`, `docs/activitypub_examples`, `docs/fep-1a11.md`); Mbin `cf95b04` (`docs/05-fediverse_developers`); NodeBB `260a0cc` (`src/activitypub/*`); discourse-activity-pub; friendica `FEDERATION.md` |
|
||
| PeerTube | docs.joinpeertube.org/api/activitypub; Chocobozzz/PeerTube develop (`object-to-model-attributes.ts`, `custom-validators/activitypub/*`, `process-*.ts`); live fetches from peertube2.cpy.re, garr.tv (8.2.4), tube.tchncs.de |
|
||
| Pixelfed, Loops | pixelfed dev (`Transformer/ActivityPub/Verb/*`, `Inbox/HandlesCreates.php`, `Helpers::verifyAttachments`); loops-server main (`FEDERATION.md`, `NoteWithVideoAttachmentValidator.php`) |
|
||
| Long-form, events, audio, books | wordpress-activitypub trunk (`class-post.php`, `class-signature.php`); TryGhost/ActivityPub v1.2.13; writefreely `posts.go`; Mobilizon 5.2.4 converters; Gancio 1.28.2/2.0-beta; Funkwhale 2.0.x; Castopod 1.15.5; Owncast 0.3.0; BookWyrm 0.9.3; NeoDB `docs/internals/activitypub.md` |
|
||
| New entrants | fed.brid.gy/docs and bridgy-fed `activitypub.py`; live probes of threads.net, flipboard.com and bsky.brid.gy; engineering.fb.com (2024-03-21) |
|
||
| FEPs | codeberg.org/fediverse/fep at 2026-09-30: 044f, 1311, 1a11, 1b12, 2345, 268d, 2c59, 4f05, 521a, 5624, 5feb, 7458, 7628, 7888, 7aa9, 844e, 8967, 8a8e, 8b32, 8fcf, 9098, 9967, ae0c, b2b8, c0e0, c16b, d556, e232, ef61, f15d, f228, fb2a, fe34 |
|
||
| Signatures | SWICG "ActivityPub and HTTP Signatures" report and its RFC 9421 adoption tracker; SocialHub "RFC 9421 HTTP signatures in 2026" (Jan 2026) |
|