# Federation PrivaPub is an ActivityPub server written in C#. This document follows [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) and describes how it federates. ## Supported federation protocols and standards - [ActivityPub](https://www.w3.org/TR/activitypub/) (server-to-server) - [WebFinger](https://webfinger.net/) - [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures), `rsa-sha256` / `hs2019` with RSA keys - [NodeInfo](https://nodeinfo.diaspora.software/) 2.0 and 2.1 ## Tested against - **GoToSocial 0.22.1, end to end.** It runs in a private network on the workstation (`tools/pasture/`) and is driven through its own client API. Checked both ways: - follows, including to a locked account; - public posts with a content warning; - replies with notifications; - likes and boosts; - direct messages; - edits and deletes; - unfollow. - **Mastodon, Misskey, Lemmy and PeerTube, by unit tests only.** The tests feed the parser documents written in each server's shape. No live exchange with any of them has run yet. ## Supported FEPs - [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) - [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) - [FEP-2c59: Discovery of a WebFinger address from an ActivityPub actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2c59/fep-2c59.md) - [FEP-1b12: Group federation](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md) (communities; see "Groups") Planned (see `docs/ROADMAP.md`, phases P5 to P8, and the per-platform notes in `docs/INTEROP.md`): FEP-044f (quotes), FEP-9967 (polls), FEP-c0e0 (emoji reactions), FEP-9098 (custom emoji), FEP-7888 and FEP-f228 (threads), FEP-7628 (Move), FEP-8fcf (followers synchronisation), FEP-5feb (`indexable`), FEP-8967 (link attachments), FEP-521a and FEP-8b32 (keys and integrity proofs), FEP-ae0c (relays). ## Actors Every account is an *avatar*: one private login can own several, and they are deliberately unlinkable. Nothing in an actor document, a collection, NodeInfo or a delivery relates two avatars of the same login. | Thing | Address | |---|---| | Actor (Person, Group, Application) | `/peasants/{name}` (`/users/{name}` redirects) | | Inbox | `/peasants/{name}/mouth` | | Outbox | `/peasants/{name}/anus` | | Shared inbox | `/human-centipede` | | Followers / following | `/peasants/{name}/groupies`, `/peasants/{name}/stalking` | | Objects | `/peasants/{name}/scribbles/{id}` | | Activities | `/peasants/{name}/grunts/{id}` | | Quote permissions (`QuoteAuthorization`) | `/peasants/{name}/parrot-licences/{id}` | | Direct-message context | `/peasants/{name}/whispers/{id}` | | Profile and post pages | `/@{name}`, `/@{name}/{id}` | The names are the project's own and are stable; resolve actors through WebFinger, not by guessing a path. - The key is `{actor}#main-key`, RSA 2048, served as SPKI PEM with `owner` set to the actor. - `published` on an actor is a whole day, chosen at random up to two weeks before the account was made, so two personas made on the same day do not share a date. `indexable` is `false`. - The instance actor is `/peasants/privapub` (type `Application`). It signs every fetch PrivaPub makes, so no avatar's key is used to read another server's content. ## Groups A group is either a **community** or a **circle**. - A **community** follows [FEP-1b12](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md). Posts addressed to it (in `to`, `cc` or `audience`) are accepted according to its posting policy (followers, anyone, or moderators only) and the group `Announce`s the whole activity, with `audience` set, to its followers. A new post is also announced as an object so Mastodon shows it. Updates and deletes of community content are announced too. A top-level post is a `Page` with a `name`. Members are counted at `/flock`; moderators are listed at `/wardens`, which the actor's `attributedTo` points to, with `postingRestrictedToMods` as Lemmy expects. A mention of a community posts into it. - A **circle** is private. Its actor is not discoverable and every follow is a request. Its posts are addressed to the circle and its members collection, delivered to each member's own inbox and never announced. Each member's copy also names that member in `cc`, so servers that keep only posts naming one of their accounts (Mastodon, GoToSocial) keep it; it names no other member. The posts are served only to a signed request from a member, or from the instance actor of a member's server, and that copy names the member (or the members on that server); anyone else gets 404. A Mastodon member's replies reach only the people they mention. - Announces from **remote** groups (Lemmy communities) are followed through to the activity: the object is fetched from its own origin, never taken from the announce. ## Activities Received: | Activity | Effect | |---|---| | `Follow` | follows an avatar or community; `Accept` is sent unless the community approves members by hand | | `Accept{Follow}`, `Reject{Follow}` | completes or ends a follow an avatar requested | | `Undo{Follow, Like, Announce}` | reverses it | | `Create{Note, Article, Page, Question, Video, Audio, Event, ChatMessage, …}` | stored when a local avatar follows the author, is addressed or mentioned, when it replies to a local post, or when it is addressed to a community the author follows; a public parent is fetched to complete the thread | | `Update{Note}` | replaces the content; the previous version is kept | | `Update{Person}` | refetches the actor | | `Like` | counted and notified, on posts the liker could see | | `Dislike` | counted as a downvote (Lemmy, PieFed, Mbin, Friendica); `Undo` takes it back | | `EmojiReact`, `Like` with an emoji `content` | an emoji reaction (FEP-c0e0; Pleroma, Akkoma, Iceshrimp.NET, Misskey, Sharkey), Unicode or a custom emoji from its `tag`; a `Like` whose content is ❤ stays a favourite; `Undo` takes it back | | `Join` | answered with `Ignore`: PrivaPub hosts no events yet (FEP-8a8e) | | `Announce` | counted and notified for local posts; shown to followers of the announcer, with the original refetched from its origin | | `Delete` | deletes the object, or the actor and its follows; a deleted object id is remembered for 90 days, so a late `Create` cannot bring it back | | `Flag` | becomes a report for this server's moderators | Sent: `Follow`, `Undo{Follow}`, `Create{Note}`, `Create{Question}` and poll votes, `EmojiReact` and its `Undo`, `Update{Note}`, `Update{Person}`, `Delete{Tombstone}`, `Accept{Follow}`, `Reject{Follow}`, `Like`, `Announce` and their `Undo`, `Flag`. A deleted post answers 410 with a `Tombstone`. - **Attachments** are `Document`s with `mediaType`, `name` (alt text), `blurhash`, `focalPoint`, `width` and `height`. Uploaded files have all metadata removed. - **Pinned posts** are the actor's `featured` collection (`/trophies`); `featuredTags` is `/tattoos`. - **Blocks are sent.** A blocked remote account receives `Block` from the blocking account (and `Reject{Follow}` if it followed); an unblock sends `Undo{Block}`. - **Reports** are sent as `Flag` by the instance actor, never by the reporting account. A `Create`'s `Note` carries Mastodon's `content`, `contentMap`, `summary` and `sensitive`, plus `Mention` and `Hashtag` tags. **Polls** (FEP-9967) go out as a `Question` with `oneOf` or `anyOf`, each option's count in `replies.totalItems`, `endTime`, `votersCount`, and `closed` once it has ended. Incoming votes are `Create{Note{name, inReplyTo}}` with no content, one per choice, counted once per voter. Counts are refreshed with an `Update{Question}` at most every three minutes. Our own votes on other servers' polls are sent the same way to the poll's author only, without `published`. An incoming `Update` without a newer `updated` only refreshes counts and details; it is never recorded as an edit. **Quotes** (FEP-044f, with the older keys): - **Received.** A quote is read from `quote`, `quoteUrl`, `quoteUri`, `_misskey_quote` or a FEP-e232 `Link` tag. - With a `quoteAuthorization`, the stamp is fetched and must be a `QuoteAuthorization` on the quoted author's origin, attributed to them, naming exactly the two posts. - A FEP-044f `quote` without a stamp is pending until an `Update` brings one. - A quote made with the older keys only, by software that asks nobody, is shown when the quoted post is public. - A `Delete` of a stamp revokes the quote. - **Sent.** Our accounts quote with every key, plus the `RE:` fallback. - When the quoted post states an `interactionPolicy.canQuote` that allows us, `quote` is included and a `QuoteRequest` (with the quoting post as `instrument`) goes to the quoted author. The quote stays pending until their `Accept` brings a stamp we can verify; the post is then updated with `quoteAuthorization`. - Posts that state no policy are quoted the older way, without `quote`. - Quoting posts are also delivered to the quoted author. - **Quoting our accounts.** Our public and unlisted posts state `interactionPolicy.canQuote`. By default anyone may quote, automatically (as on Mastodon); an account can choose followers only or nobody, and so can each post. - A `QuoteRequest` is answered with `Accept{result}` naming a stamp at `/peasants/{name}/parrot-licences/{id}`, or with `Reject`. - A stamp is a `QuoteAuthorization` naming both posts. A revoked stamp answers 410 and is announced with `Delete{stamp}`. - Followers-only posts and DMs can never be quoted. **Custom emoji** (`Emoji` tags) are read on posts, display names, bios and profile fields, at most 64 per object. **Profiles** keep their header, profile fields, `manuallyApprovesFollowers`, `published`, `movedTo`, `indexable`, `memorial` and avatar and header descriptions. A post's title becomes `name` and is also the first, bold line of `content`, because Mastodon does not show `name`. A content warning without its own text uses the title, or "Content warning". Received `summary` is a content warning only on a `Note` or `Question`, or when `sensitive` is `true`. On an `Article`, `Page`, `Event`, `Video` or `Audio` it is an excerpt or description, kept as such and never hidden. Visibility is expressed in `to`/`cc` the way Mastodon does it: public, unlisted, followers-only and direct. Inbound followers-only posts are recognised by the author's own `followers` collection. ## Link previews For a public post that links to a page, PrivaPub builds a preview card. It uses, in order: 1. a Link attachment's own `preview` (FEP-8967) or the post's own image, title and summary; 2. otherwise, the linked page, read once by the server between 0 and 60 seconds after the post arrives (never when someone reads it), with an `Accept: text/html` request that reads at most 512 KB, through the same address checks as every other fetch. The page's OpenGraph and Twitter tags give the title, description and image; the result is cached per address for 7 days and shared by every account on the server. Images are served to clients only through PrivaPub's media proxy. `Federation:FetchLinkPreviews=false` turns page fetching off. ## Server descriptions and the crawler PrivaPub keeps statistics about servers, never about accounts (see `/stargazing` on the server). - **Describing a server:** a server it exchanges activities with is described at most once a week, from: - `/.well-known/nodeinfo` and the NodeInfo document it links; - `/api/v2/instance`, falling back to `/api/v1/instance`. These requests are unsigned, because they are not ActivityPub documents. - **The crawler** is on at privapub.thepra.dev. It identifies as `PrivaPub-Stargazer/ (+https://privapub.thepra.dev/stargazing)`. - **What it reads:** `/robots.txt`, then the documents above and `/api/v1/instance/peers`, and nothing else: no accounts, posts or directories. - **How often:** one server a minute, each at most weekly. - **Opting out:** in robots.txt, the crawler obeys the group `PrivaPub-Stargazer`, then `PrivaPub`, then `*`. A robots.txt that answers with a server error or times out also keeps it out, as do this server's domain blocks. ## Local-only posts Posts with a location (shown to nearby users of this server) never leave the server, in any form. ## Security rules a peer will notice - **Signatures.** Inbox POSTs must be signed over `(request-target)`, `host`, `digest` and `date` (or `(created)`). The date may be at most one hour old and fifteen minutes ahead. A bad signature gets 401, malformed input 400, an accepted activity 202, too many requests 429. Activities are processed after the 202. - **Origins.** An actor document is accepted only from the address it names as its `id`. A key only if its actor lists it with `owner` set to the actor and on the actor's origin. An activity's `id`, and any object it creates, updates or deletes, must be on its actor's origin. An embedded object from another origin is fetched from that origin. - **Fetching.** All fetches are signed by the instance actor. They go only to public addresses, follow at most three redirects and read at most 1 MB. - **Reading our documents (SecureMode).** privapub.thepra.dev answers ActivityPub GETs only when they are signed, like Mastodon's authorized fetch; the instance actor `/peasants/privapub` is the exception, since its key is needed first. A browser asking for HTML is redirected to the public page instead. - **Posts that are not public** (followers-only, direct, circle) are served to a signed request from someone they were for, or from the instance actor of a server where someone they were for lives, and to nobody else (404). Once deleted they answer those same readers 410. A direct message's `context`, `/peasants/{name}/whispers/{id}`, is an `OrderedCollection` of the conversation's posts for its participants. - **HTML.** Received HTML is sanitised to Mastodon's allowlist. - **Keys we cannot fetch for now.** When a sender's key cannot be fetched because its server timed out or answered 5xx, the inbox answers 503 with `Retry-After: 300` rather than 401. - **Activity ids.** `Create` and `Announce` ids dereference, a community's announces included, until the post they carry is deleted (then 410); a persona's boost id sends a browser to the boosted post. `Follow`, `Like`, `Block` and the `Accept`, `Reject` and `Undo` that answer them do not, because serving them would reveal who follows, likes and blocks whom. Neither do `Update`, `Delete`, `EmojiReact`, `QuoteRequest` and its answers, `Flag`, `Ignore` or poll votes. All of them are always sent with their object embedded. - **Hashtags.** A post's `Hashtag` links go to `/tags/{tag}`, a public page of this server's public posts with that tag. - **Delivery.** Failed deliveries are retried with Mastodon's backoff (16 attempts). A host that keeps failing is paused, starting at an hour and growing to a week. ## Known limitations - Our own posts carry no custom emoji: emoji are received and shown, not offered. - Remote media is fetched through this server's proxy when a local client displays it. Video and audio are streamed through it with byte ranges passed on, so a PeerTube file is never fetched whole for one viewer. - Collections expose counts, not members: `/groupies` and `/stalking` give the same totals as the Mastodon API's follower and following counts, and the outbox's `totalItems` is the persona's post count, though only public posts are listed. `/api/v1/instance/peers` is empty: which servers this one talks to is not published. - Only `rsa-sha256`-style keys are verified. RFC 9421 signatures are planned.