- Our public and unlisted posts state interactionPolicy.canQuote. The policy comes from the post, then the persona
(`source[quote_policy]`: public, followers or nobody; default public as the owner chose), and is always nobody for
followers-only posts and DMs.
- QuoteRequests are answered with Accept{result} naming a parrot-licence at /peasants/{name}/parrot-licences/{id}
(the route name the owner chose), or with Reject. Followers-only checks that the requester really follows.
- A licence is a QuoteAuthorization naming both posts; revoking it (POST /api/v1/statuses/:id/quotes/:quoting_id/revoke)
marks it 410, sends Delete{licence} to the quoter and the persona's followers, and revokes our own copy of the quote.
- A quote that arrives with one of our licences is accepted only if that licence is ours, unrevoked and names exactly
that quoting post. One persona quoting another gets a licence too.
- Mastodon API: quote_approval for our posts (automatic, followers, current_user), `quote_approval_policy` when posting,
PUT /api/v1/statuses/:id/interaction_policy, `source.quote_policy`.
Checked live: GoToSocial still accepts our posts with the policy stated, and leaves likes, replies and boosts open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
13 KiB
Federation
PrivaPub is an ActivityPub server written in C#. This document follows FEP-67ff and describes how it federates.
Supported federation protocols and standards
- ActivityPub (server-to-server)
- WebFinger
- HTTP Signatures,
rsa-sha256/hs2019with RSA keys - NodeInfo 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
- FEP-f1d5: NodeInfo in Fediverse Software
- FEP-2c59: Discovery of a WebFinger address from an ActivityPub actor
- FEP-1b12: Group federation (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 withownerset to the actor. publishedon 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.indexableisfalse.- The instance actor is
/peasants/privapub(typeApplication). 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. Posts
addressed to it (in
to,ccoraudience) are accepted according to its posting policy (followers, anyone, or moderators only) and the groupAnnounces the whole activity, withaudienceset, 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 aPagewith aname. Members are counted at/flock; moderators are listed at/wardens, which the actor'sattributedTopoints to, withpostingRestrictedToModsas 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. They are served only to a signed request from a member, or from the instance actor of a member's 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
Documents withmediaType,name(alt text),blurhash,focalPoint,widthandheight. Uploaded files have all metadata removed. - Pinned posts are the actor's
featuredcollection (/trophies);featuredTagsis/tattoos. - Blocks are sent. A blocked remote account receives
Blockfrom the blocking account (andReject{Follow}if it followed); an unblock sendsUndo{Block}. - Reports are sent as
Flagby 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_quoteor a FEP-e232Linktag.- With a
quoteAuthorization, the stamp is fetched and must be aQuoteAuthorizationon the quoted author's origin, attributed to them, naming exactly the two posts. - A FEP-044f
quotewithout a stamp is pending until anUpdatebrings one. - A quote made with the older keys only, by software that asks nobody, is shown when the quoted post is public.
- A
Deleteof a stamp revokes the quote.
- With a
- Sent. Our accounts quote with every key, plus the
RE:fallback.- When the quoted post states an
interactionPolicy.canQuotethat allows us,quoteis included and aQuoteRequest(with the quoting post asinstrument) goes to the quoted author. The quote stays pending until theirAcceptbrings a stamp we can verify; the post is then updated withquoteAuthorization. - Posts that state no policy are quoted the older way, without
quote. - Quoting posts are also delivered to the quoted author.
- When the quoted post states an
- 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
QuoteRequestis answered withAccept{result}naming a stamp at/peasants/{name}/parrot-licences/{id}, or withReject. - A stamp is a
QuoteAuthorizationnaming both posts. A revoked stamp answers 410 and is announced withDelete{stamp}. - Followers-only posts and DMs can never be quoted.
- A
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:
- a Link attachment's own
preview(FEP-8967) or the post's own image, title and summary; - 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/htmlrequest 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.
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,digestanddate(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 withownerset to the actor and on the actor's origin. An activity'sid, 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.
- 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: 300rather than 401. - Activity ids.
CreateandAnnounceids dereference.Follow,Like,Blockand theAccept,RejectandUndothat answer them do not, because serving them would reveal who follows, likes and blocks whom; they are always sent with their object embedded. - 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.
- Only
rsa-sha256-style keys are verified. RFC 9421 signatures are planned.