Aller au contenu

ActivityPub client API

API et applications 11 min de lecture Source sur codefloeEn anglais uniquement

Sur cette page

Catodon implements the client-to-server half of ActivityPub (W3C REC §5–§6), following the SocialCG ActivityPub API Task Force's Basic Profile for ActivityPub API Servers. A generic ActivityPub app can sign an account in with OAuth, read its inbox and outbox, and post activities to its outbox. Each posted activity is carried out through the same code the Misskey and Mastodon APIs use, so it has the same effects, limits and policies as the same action taken in the web client.

This page is for developers of such apps and for administrators. The exact behaviour of every activity, including what Catodon's data model cannot express, is in packages/backend/src/server/activitypub-c2s/COMPATIBILITY.md.

Turning it on or off

The API is on by default. An administrator can turn it off in Control Panel > Federation > Policy > ActivityPub client API (the meta setting enableC2S). While it is off:

  • actors publish no OAuth or C2S endpoints, and the OAuth server metadata leaves out the task force's scopes and the FEP-d8c2 flag;
  • the C2S routes answer exactly as if they did not exist (404, or the web client's page for GET /users/:id/inbox, /liked, /lists/:list and /media/:id), and a bearer token on a route shared with federation changes nothing;
  • apps identified by a web address can still sign in, for Misskey permissions and basic Mastodon scopes only.

Discovery

An app starts from the account's actor (by WebFinger or its id). With the API on, the actor carries:

Property Value
inbox, outbox /users/<id>/inbox, /users/<id>/outbox
liked /users/<id>/liked (readable by the owner only)
endpoints.oauthAuthorizationEndpoint /oauth/authorize
endpoints.oauthTokenEndpoint /oauth/token
endpoints.uploadMedia /c2s/upload-media
endpoints.proxyUrl /c2s/proxy
objectIDAsClientID true (FEP-d8c2)

The same endpoints are in the RFC 8414 document at /.well-known/oauth-authorization-server, with code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none", "client_secret_post"], client_id_metadata_document_supported: true, activitypub_object_id_as_client_id: true and the scopes in scopes_supported. Group (forum) and system actors publish none of this: nobody signs in as them.

Signing in

OAuth 2.0 authorization code grant, with PKCE (S256) required. There is no client secret and nothing to register: the client_id is an https: URL that describes the app, fetched when the user is asked to authorize it. Three kinds of document are understood:

  • a Client ID Metadata Document: JSON with client_id (equal to its own URL), redirect_uris, client_name, logo_uri, client_uri;
  • an ActivityPub Application or Service (FEP-d8c2) whose id is the client_id, with redirectURI (a string or a list), name or nameMap, icon and summary;
  • an IndieAuth page: an h-app and <link rel="redirect_uri">, or Link headers.

The redirect_uri must match one the document lists exactly. The document is fetched with the server's usual protections: private and loopback addresses are refused unless the administrator allowed them, redirects are followed one at a time and checked each time, and the document may be at most 1 MiB.

The user sees a consent page with the app's name and icon, the server the app is published on, the one they will be sent back to, and the permissions asked for. The permissions cannot be unticked one by one: the user accepts them all or refuses. The redirect carries code, state and iss (RFC 9207). The code is valid for five minutes and only once: exchanging it twice fails and revokes the token the first exchange bought.

The token response:

{
  "access_token": "…",
  "token_type": "Bearer",
  "scope": "https://swicg.github.io/activitypub-api/scopes#readinbox https://swicg.github.io/activitypub-api/scopes#createcontent …",
  "created_at": 1790579372,
  "activitypub_actor_id": "https://example.social/users/9xyz…",
  "me": "https://example.social/@alice"
}

scope is what was granted, which may be less than what was asked for. Admin permissions (read:admin:*, write:admin:*) are never granted to an app identified by a web address, whatever it asks for. Tokens do not expire, so no refresh token is issued. Dynamic client registration (RFC 7591) is not offered; a URL client_id covers that case. The app then appears in the user's Settings > Connected apps, where it can be revoked. Mastodon apps keep signing in through the Mastodon flow on the same endpoints (a client_id that is not a URL); nothing changed for them.

Send the token as Authorization: Bearer <token>. It is never read from a cookie or a query parameter.

Scopes

An app may ask for the task force's 28 scopes (https://swicg.github.io/activitypub-api/scopes#<name>), for Misskey permissions (read:account, write:notes, …) and for Mastodon's read, write and follow. A token granted only task-force scopes can use nothing but this API: they are not turned into Misskey permissions that would also open the Misskey or Mastodon API.

Scope Allows
readall read your inbox, outbox, followers, following and liked (your own data only)
readinbox, readoutbox, readfollowers, readfollowing, readliked read that collection
readlocal, readown, readany read posts on this server, on the app's own server (its client_id host), or anywhere; also the reach of proxyUrl. readany includes the other two
createcontent create posts, including replies and quotes to posts on any server, blog posts and poll votes
updatecontent, deletecontent edit and delete your posts and blog posts
follow follow and unfollow
managefollowers accept, reject and remove followers
managecollections pin and unpin posts; create, rename and delete lists and change their members
like, share like (react) and repost anywhere, and take them back
reactlocal, reactown, reactany like, repost, reply to, quote and vote in posts within their reach, for an app without like, share or createcontent. reactany includes the other two
block, flag block and unblock; report
addresspublic, addressfollowers, addressactor address a post to the public, to your followers, or to individual actors. addressall includes all three. A post addressed to nobody needs none
uploadfiles upload media
updateprofile change your profile, including avatar and banner

A token without any task-force scope (a Mastodon app token, a MiAuth token, a URL client that asked only for Misskey permissions) is limited by its Misskey permissions alone.

Endpoints

Method and route Scope What it does
POST /users/:id/outbox per activity (below) carry out an activity; 201 with Location and the resulting activity
GET /users/:id/inbox readinbox the owner's home timeline, as activities; owner only
GET /users/:id/outbox readoutbox with the owner's token, every post of any visibility
GET /users/:id/liked readliked the posts the owner reacted to; owner only
GET /users/:id/followers, /following readfollowers, readfollowing with the owner's token, whatever the profile's visibility setting
GET /users/:id/collections/featured readoutbox with the owner's token, pinned posts of any visibility
GET /users/:id/lists/:list managecollections or readall one of the owner's lists: its members; owner only
GET /notes/:id, /notes/:id/activity a read reach, or readoutbox for your own any post the token's user may see
GET /notes/:id/replies, /likes, /shares, /context the same a post's replies and thread as the token's user may see them, and its like and share counts
GET /@user/blog/:slug the same a redirect to the blog post's id, for any blog post the token's user may see
POST /c2s/upload-media uploadfiles multipart/form-data with file and object (JSON); 201 with the new Image, Video, Audio or Document, whose id is /media/<id>
GET /media/:id the uploaded object: to the owner, and to anyone once a public post attaches it
POST /c2s/proxy readlocal, readown or readany application/x-www-form-urlencoded id=<url>: the object, fetched signed as the account

The outbox is addressed by the actor's id, as its outbox property names it. Collections are paged with ?page=true and since_id / until_id, newest first, 20 a page; followers and following use ?page=true&cursor=…, 10 a page; replies (until_id) and the thread (since_id, oldest first) 50 a page. A browser opening the inbox, liked or a list without a token gets the web client's page; every other request without one gets 401. Every answer to a token request carries Cache-Control: private, no-store and Vary: Authorization.

The outbox takes application/ld+json; profile="https://www.w3.org/ns/activitystreams" or application/activity+json, at most 256 KB. Anything else is 415.

Activities

A bare object is wrapped in a Create, with its addressing copied both ways. Client-supplied ids are ignored. actor, and a created object's attributedTo, must be absent or the account's own actor.

Posted Effect Scope
Create of Note (or Image, Video, Audio: a post with attachments) a post. inReplyTo, the quote (quote, quoteUrl, quoteUri, _misskey_quote), summary (content warning), sensitive, attachment (files from uploadMedia), source (text/x.misskeymarkdown, text/markdown, text/plain) or HTML content, contentMap (language) createcontent + address*
Create of Question a post with a poll (oneOf / anyOf, endTime) createcontent + address*
Create of a Note with only a name, in reply to a poll a vote createcontent, or a react* reaching the poll
Create of Article a blog post (public or followers only) createcontent + address*
Create of Collection / OrderedCollection a private list managecollections
Update of your post, blog post, list or actor a partial update: given properties replace, null deletes updatecontent, managecollections, updateprofile
Delete of your post, blog post or list deletes it deletecontent, managecollections
Follow of an actor follows (or requests to) follow
Accept / Reject of a Follow of you answers the request; Reject of an accepted follower removes it managefollowers
Like, EmojiReact of a post the default reaction, or the one in content like, or a react* in reach
Announce of a post a repost (public, non-discoverable public or followers) share, or a react* in reach, + address*
Block of an actor blocks block
Flag of an account and its posts a report (content is the comment) flag
Add / Remove with your featured collection as target pins / unpins managecollections
Add / Remove with your list as target adds / removes a member managecollections
Remove with your followers as target removes a follower managefollowers
Undo of your Follow, Like, Announce, Block takes it back; by our id or embedded the scope that made it
anything else 422, with the supported list

The response's Location is the resulting activity's id. Create, Announce, Follow and Like ids dereference; the ids of Update, Delete, Undo, votes and blocks do not yet. Details per activity: COMPATIBILITY.md.

Errors

Every error from the C2S routes is application/problem+json (RFC 9457): type (about:blank), title, status, detail, and code, the Misskey API's error code or a C2S one (UNSUPPORTED_ADDRESSING, HIDDEN_RECIPIENTS_UNSUPPORTED, UNSUPPORTED_ACTIVITY, NO_SUCH_OBJECT, …). A 422 says what could be expressed instead (expressible, supported, properties).

Status When
401 no token or an invalid one; WWW-Authenticate: Bearer
403 the token's user is not the path's owner (NOT_OWNER), a missing scope (INSUFFICIENT_SCOPE, with the scope URI, also in WWW-Authenticate), a missing Misskey permission, a suspended account
404 not found, or not visible to this token (the two are not told apart)
413, 415 the body is too large, or not ActivityPub JSON
422 the activity cannot be carried out as asked
429 rate limited, with Retry-After

The rate limits are those of the Misskey API endpoints each activity uses.

CORS

The C2S routes answer preflights and send Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, POST, OPTIONS, Access-Control-Allow-Headers: Accept, Authorization, Content-Type and Access-Control-Expose-Headers: Location, Vary, Retry-After plus the rate-limit headers, so a browser app on any origin can use the API. Credentials are bearer tokens, never cookies, which is what makes * safe.

Caveats

  • A Block is delivered to the blocked account's server. ActivityPub says a server must not deliver it; Catodon keeps one behaviour for all its APIs, and the Misskey and Mastodon APIs deliver it for compatibility with Mastodon.
  • Addressing Misskey cannot represent is refused, not dropped. A post is public, non-discoverable public, followers-only or direct. An individual actor on a public, home or followers post must be mentioned in the text (or be the author of the post replied to or quoted). bto and bcc may only repeat addressees already in to or cc, because a recipient cannot be hidden. Another account's collection or a list cannot be addressed. Each of these is a 422 that says what is expressible.
  • A post addressed to nobody is visible to its author alone, not public.
  • Content is stored as Misskey text. HTML content is converted, and the object is served back as Catodon renders it (<p>v1</p> for v1), with the text in source. Send source to control it exactly. A post needs text, files or a poll; an empty one is refused.
  • A deleted post answers 404, not 410 with a Tombstone. A post a moderator removed is a Tombstone, its activity 404, and it is left out of every collection.
  • The inbox is synthesised from the home timeline; received activities are not stored, and notifications (follow requests, likes of your posts) are not in it.
  • Activity types without an effect in Catodon (Listen, Arrive, …) are refused with 422 rather than stored and delivered.

Known gaps

Found while trying the API with the ap command-line client (evanp/ap) and the socialweb.coop test suite:

  • A post's likes and shares collections carry counts only, as Mastodon's do. A client that finds its own Like or Announce through them to undo it cannot; send Undo with the id from the Location of the Like or Announce, or with the activity embedded ({"type": "Like", "object": "<post>"}).
  • There are no pendingFollowers / pendingFollowing collections, and the outbox lists posts and reposts, not Follow or Like activities. Follow requests are answered by Accept / Reject of the Follow embedded or by its id.