├── LICENSE ├── README.md ├── announce-post.md ├── create-post.md ├── delete-post.md ├── follow-post.md ├── move-post.md ├── user.md └── webfinger.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ben Boyter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActivityPub Explained 2 | 3 | ``` 4 | /\ | | (_) (_) | 5 | / \ ___| |_ ___ ___| |_ _ _ 6 | / /\ \ / __| __| \ \ / / | __| | | | 7 | / ____ \ (__| |_| |\ V /| | |_| |_| | 8 | /_/ \_\___|\__|_| \_/ |_|\__|\__, | 9 | | __ \ | | __/ | 10 | | |__) | _| |__ |___/ 11 | | ___/ | | | '_ \ 12 | | | | |_| | |_) | 13 | |_| \__,_|_.__/ 14 | ``` 15 | 16 | Sequence diagrams of how ActivityPub/Webfinger works and how to implement against it will be stored here. This is needed because the spec is vague and different instances implement things in different ways. 17 | 18 | The goal is to create a collection that can be used by someone in a clean room environment to implement an instance that could work against the fediverse. 19 | 20 | - [Announce Post](announce-post.md) - Boost/Retweet/Share 21 | - [Create Post](create-post.md) - Toot/Tweet/Post 22 | - [Move Post](move-post.md) - User Migration 23 | - [Delete Post](delete-post.md) - Toot/Tweet/Post Deletion 24 | - [Follow Post](follow-post.md) - Follow Account 25 | - [Webfinger](webfinger.md) - Webfinger 26 | - [User](user.md) - Getting user details 27 | 28 | PR's are encouraged! 29 | 30 | 31 | The following are good to look through, 32 | 33 | - https://www.w3.org/TR/activitypub/ 34 | - https://docs.joinmastodon.org/spec/activitypub/ 35 | - https://activitypub.rocks/ 36 | 37 | The following are implementations in a few different languages, 38 | 39 | - https://github.com/jointakahe/takahe Python 40 | - https://humungus.tedunangst.com/r/honk Go 41 | - https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/ JavaScript 42 | - https://github.com/benbrown/shuttlecraft JavaScript 43 | 44 | Some specific behaviors are covered well here. 45 | 46 | - https://docs.gotosocial.org/en/latest/federation/behaviors/outbox/ 47 | -------------------------------------------------------------------------------- /announce-post.md: -------------------------------------------------------------------------------- 1 | # Announce -> Post 2 | 3 | What follows is how a boost/announce/retweet works in Mastodon and any other system that implements against ActivityPub. 4 | 5 | When a user clicks on bonk/boost/retweet the following sequence happens. 6 | 7 | The users intent is converted to an announce. This announce is sent to every follower they have as well as the owner 8 | of the post that the user is boosting. 9 | 10 | In the case of follower instances, they receive the announce, then fetch the content of the announce using the `object` field. 11 | 12 | In the case of the owner, they take the announce and increment their boost count. 13 | 14 | 15 | ```mermaid 16 | sequenceDiagram 17 | actor User 18 | User->>Instance: Announce 19 | Instance--)Instance: Fetch Following 20 | loop Every Follower 21 | Instance--)Remote Instance: Announce to follower 22 | Remote Instance--)Owner Instance: Fetch Content 23 | end 24 | Instance--)Owner Instance: Announce 25 | Owner Instance--)Owner Instance: Increment Count 26 | ``` 27 | 28 | 29 | An example announce appears like the below. Actor is whoever did the boost, cc is the user who's post is being boosted and object contains a link to the post itself. The below assumes the instance honk.boyter.org was boosting a post from another instance named some.instance in this case. 30 | 31 | ```json 32 | { 33 | "@context": "https://www.w3.org/ns/activitystreams", 34 | "actor": "https://honk.boyter.org/u/boyter", 35 | "cc": [ 36 | "https://some.instance/users/someonemadup" 37 | ], 38 | "context": "tag:mastodon.social,2023-01-17:objectId=380216002:objectType=Conversation", 39 | "id": "https://honk.boyter.org/u/boyter/bonk/234wetsdfw35345", 40 | "object": "https://some.instance/users/someonemadup/statuses/1231235634623412", 41 | "published": "2023-01-19T22:41:49Z", 42 | "to": "https://www.w3.org/ns/activitystreams#Public", 43 | "type": "Announce" 44 | } 45 | ``` 46 | 47 | When fetching content for the above every remote instance needs to call back to the owner instance in order to fetch the content for display. Instances can require a signed request for this to ensure only followers can see the post, and return 404 generally 48 | 49 | The fetch is done using the object value, with the appropriate header sent. 50 | 51 | ```bash 52 | curl --location --request GET 'https://some.instance/users/someuser/statuses/123413513412312' --header 'Accept: application/json' 53 | ``` 54 | 55 | Note that the header should be set to `application/activity+json` or `application/ld+json; profile="https://www.w3.org/ns/activitystreams` to be even more precise, but that some instance 56 | implementations such as honk will ignore `application/json` for this and not return JSON. This seems to vary by instant type. 57 | 58 | - Mastodon - works with both `application/activity+json` and `application/json` and `application/ld+json; profile="https://www.w3.org/ns/activitystreams` 59 | - Takahe - works with both `application/activity+json` and `application/json` 60 | - Honk - withs with `application/activity+json` and `application/ld+json; profile="https://www.w3.org/ns/activitystreams` 61 | 62 | It is vital that the owner instance caches the result for these GET requests as popular content could be queried 63 | many times before a copy being made on all the remote instances. 64 | -------------------------------------------------------------------------------- /create-post.md: -------------------------------------------------------------------------------- 1 | # Create -> Post 2 | 3 | Posts tend to be created with a few possible states before being distributed, in Mastodon and other systems. 4 | 5 | - Public. This will appear on the local timeline, and go to all followers of the user, and any @ mentions. Anyone can access this post via API. 6 | - Mentioned. This will only go to a direct @ mentions in the message itself, and not followers. Such posts should only be accessable to signed requests for the person @ via API. 7 | - Followers. This will only go to followers, and not appear on the local timeline. Such posts should only be accessable to signed requests by followers via API. 8 | - Unlisted this goes into everything, but should not allow boosts or other such actions. 9 | 10 | takahe also has the following 11 | 12 | - Local Only. This will appear on the local timeline, and go to any local followers. 13 | 14 | Rules tend to roll up, so an @ will work in any public, any mentioned or followers for example. 15 | 16 | When a post is created by a user the instance performs the following actions. 17 | 18 | - Webfinger any remote instance to get the users address 19 | - Get the users details in order to know the inbox 20 | - Message the inbox 21 | 22 | For local instances the decision is up to the instance. 23 | 24 | ```mermaid 25 | sequenceDiagram 26 | actor User 27 | User->>Instance: Create Post 28 | Instance--)Remote Instance: Webfinger 29 | Instance--)Remote Instance: User Details 30 | Instance--)Remote Instance: Create Post 31 | ``` 32 | 33 | Generally the results from webfinger and the user probes are cached to avoid hitting the remote instance again. This information should be possible to cache forever. 34 | 35 | Webfinger can be done with a simple get request 36 | 37 | `https://some.instance/.well-known/webfinger?resource=acct:someone@some.instance` 38 | 39 | User can be fetched by finding the `rel` identified as `self` inside the `links` array. This can then be fetched using the usual headers, `application/activity+json` or `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`. However note that some instance types will work with `application/json` although this is not strictly correct. 40 | 41 | ``` 42 | curl --location --request GET 'https://some.instance/users/someone' \ 43 | --header 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"' 44 | ``` 45 | 46 | The return from the above will contain a field inbox, which in the case of a mastodon system looks like the following `"inbox": "https://some.instance/users/someone/inbox"`. It is this URL that the create must be sent to as a POST. 47 | 48 | While technically not a requirement most instances require that any incoming message is signed using the users private key to ensure the message was not forged. This signature is either set via the field `signature` in the json document sent or via the `Signature` HTTP header. Most fediverse platforms create HTTP signatures according to [draft-cavage-http-signatures-12](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) specification, which differs significantly from the latest version of the standard. 49 | 50 | ## Bare Minimum 51 | 52 | The bare minimum you need to send a message is the following for any instance that verifies the signature. 53 | 54 | - Public domain accessable by the internet at large, or the private key for a public domain account. 55 | - Webfinger implementation 56 | - User endpoint implementation 57 | 58 | With the above in place you should be able to create a message, sign it, send it and have the message verified by the remote instance. 59 | 60 | With no verification in place it should be as simple as crafting the appropiate JSON object and then POST at the correct URL. 61 | 62 | The below will send a message to the inbox of https://mastinator.com/u/testinbox which you can visit. 63 | 64 | Note that the `ShouldBeUniqueDuplicatesIgnoredTillDeleted` must be unique per message, otherwise it will be ignored. The content of the below message will be `Hello!`. 65 | 66 | ```bash 67 | curl --location --request POST 'https://mastinator.com/u/testinbox/inbox' \ 68 | --header 'Content-Type: application/json' \ 69 | --data-raw '{ 70 | "@context": "https://www.w3.org/ns/activitystreams", 71 | "actor": "https://mastinator.com/", 72 | "id": "ShouldBeUniqueDuplicatesIgnoredTillDeleted", 73 | "object": { 74 | "content": "Hello!", 75 | "conversation": "empty", 76 | "id": "ShouldBeUniqueDuplicatesIgnoredTillDeleted", 77 | "published": "2022-12-20T06:03:41Z", 78 | "summary": "", 79 | "to": "https://www.w3.org/ns/activitystreams#Public", 80 | "type": "Note", 81 | "url": "https://mastinator.com/" 82 | }, 83 | "published": "2022-12-20T06:03:41Z", 84 | "to": "https://www.w3.org/ns/activitystreams#Public", 85 | "type": "Create" 86 | }' 87 | ``` 88 | -------------------------------------------------------------------------------- /delete-post.md: -------------------------------------------------------------------------------- 1 | # Delete -> Post 2 | 3 | https://www.w3.org/TR/activitypub/#delete-activity-outbox 4 | 5 | Note that deletes should only apply to [create](create-post.md) where-as undo should be used on like/follow/block and remove on add. 6 | 7 | For delete, when sent the instance should find the appropiate posts via the supplied id and either delete it or tombstone it. The activitypub spec does not define what it means by tombstone, but in the context of active directory it means to mark it for deletion, not display it anymore and remove it at a later date. 8 | 9 | In the case of mastodon the object contains a dictionary where id `object.id` is the id that should be used to delete the object created. Other instances have set the `id` in the supplied JSON. 10 | 11 | Examples below of deletes with the id `ShouldMatchTheIdOfObjectInCreate` being the one to delete in the system. 12 | 13 | Mastodon delete example 14 | 15 | ```json 16 | { 17 | "@context": "https://www.w3.org/ns/activitystreams", 18 | "actor": "https://some.instance/u/testinbox", 19 | "cc": 20 | [ 21 | "https://some.instance/u/Etiolate" 22 | ], 23 | "id": "https://some.instance/u/testinbox/217h4NBLrMJzT3qz7B#delete", 24 | "object": 25 | { 26 | "atomUri": "https://some.instance/u/testinbox/217h4NBLrMJzT3qz7B", 27 | "id": "ShouldMatchTheIdOfObjectInCreate", 28 | "type": "Tombstone" 29 | }, 30 | "published": "2023-01-05T09:26:18Z", 31 | "to": "https://www.w3.org/ns/activitystreams#Public", 32 | "type": "Delete" 33 | } 34 | ``` 35 | 36 | Other instance delete example 37 | 38 | ```json 39 | { 40 | "@context": "https://www.w3.org/ns/activitystreams", 41 | "actor": "https://some.instance/u/testinbox", 42 | "cc": 43 | [ 44 | "https://some.instance/u/testinbox" 45 | ], 46 | "id": "ShouldMatchTheIdOfObjectInCreate", 47 | "object": "https://some.instance/u/testinbox/217h4NBLrMJzT3qz7B", 48 | "published": "2023-01-05T09:26:18Z", 49 | "to": "https://www.w3.org/ns/activitystreams#Public", 50 | "type": "Delete" 51 | } 52 | ``` -------------------------------------------------------------------------------- /follow-post.md: -------------------------------------------------------------------------------- 1 | # Follow -> Post 2 | 3 | 4 | Published time should be in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) and look like the following `2006-01-02T15:04:05Z07:00`. 5 | 6 | The follow accept is not actually required for a follow to work, but is usually done in order to confirm that the follow request has been accepted. In the case where a instance user has set to manually approve follow requests this process can take an indefinate amount of time. 7 | 8 | ```mermaid 9 | sequenceDiagram 10 | actor User 11 | User->>Instance: Follow Request 12 | Instance--)Remote Instance: Webfinger 13 | Instance--)Remote Instance: User 14 | Instance--)Remote Instance: Follow Post 15 | Remote Instance--)Instance: Webfinger 16 | Remote Instance--)Instance: User Details 17 | Remote Instance--)Instance: Follow Accept 18 | ``` 19 | 20 | Example follow request to be POST'ed to a user we want to follow. 21 | 22 | 23 | ```json 24 | { 25 | "@context": "https://www.w3.org/ns/activitystreams", 26 | "actor": "https://some.instance/u/followinguser", 27 | "id": "https://some.instance/u/followinguser/sub/someuniqueid", 28 | "object": "https://another.instance/u/userwewanttofollow", 29 | "published": "2006-01-02T15:04:05Z07:00", 30 | "to": "https://some.instance/u/%s/inbox", 31 | "type": "Follow" 32 | } 33 | ``` 34 | 35 | 36 | TODO - Example of follow accept here 37 | 38 | 39 | Technically there is no need to keep track of who an instance follows, as this information is retained by the remote instance. It is probably useful to retain for displaying to the user however. -------------------------------------------------------------------------------- /move-post.md: -------------------------------------------------------------------------------- 1 | # Move -> Post 2 | 3 | Specific to Mastodon https://docs.joinmastodon.org/spec/activitypub/#supported-activities-for-profiles as it is not included in the main w3 spec https://www.w3.org/TR/activitypub/ 4 | 5 | Migrate followers from one account to another. When an account has this set they send the move request to all of their followers indicating that they should unfollow the old account and follow the new one. Previously created posts, and the public/private key that mastodon uses are not migrated. 6 | 7 | An example announce appears like the below. The user at old.instance wants to indicate that they have moved their account to new.instance. 8 | 9 | ```json 10 | { 11 | "@context": "https://www.w3.org/ns/activitystreams", 12 | "id": "https://old.instance/users/someusername#moves/1", 13 | "type": "Move", 14 | "target": "https://new.instance/users/someothername", 15 | "actor": "https://old.instance/users/someusername", 16 | "object": "https://old.instance/users/someusername" 17 | } 18 | ``` 19 | 20 | Note that the move must be signed using the private key of the user before being sent to the followers allowing verification that the request was sent by that account. This must be enfoced to avoid abuse. 21 | 22 | 23 | ```mermaid 24 | sequenceDiagram 25 | actor User 26 | User->>Instance Old: Move 27 | Instance Old--)Instance Old: Fetch Following 28 | loop Every Follower 29 | Instance Old--)Remote Instance: Move 30 | Remote Instance--)Instance Old: Unfollow 31 | Remote Instance--)Instance New: Follow 32 | end 33 | ``` 34 | 35 | -------------------------------------------------------------------------------- /user.md: -------------------------------------------------------------------------------- 1 | # User 2 | 3 | Fetching a users details usually follows a webfinger against the user to validate they exist. 4 | 5 | Given a valid webfinger such as the following 6 | 7 | https://mastinator.com/.well-known/webfinger?resource=acct:someaccount@mastinator.com 8 | 9 | We can then query their self location to determine their inbox, 10 | 11 | ```json 12 | { 13 | "subject": "acct:someaccount@mastinator.com", 14 | "links": [ 15 | { 16 | "rel": "self", 17 | "type": "application/activity+json", 18 | "href": "https://mastinator.com/u/someaccount" 19 | } 20 | ] 21 | } 22 | ``` 23 | 24 | The important part in the above is the `links.rel` where `self` indicates that the `href` for this object contains the location of this users details. 25 | 26 | We can then issue a get request to this endpoint to get the details for the user. 27 | 28 | ```json 29 | curl --location --request GET 'https://mastinator.com/u/someaccount' \ 30 | --header 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams" 31 | ``` 32 | 33 | For mastodon the accept headers of `application/activity+json`, `application/ld+json; profile="https://www.w3.org/ns/activitystreams` or `application/json` will work for the above call, but many instance types will reject the first two. It is safer to use the first one which strictly speaking is more correct anyway. 34 | 35 | Note that fetching the url without this header will usually result in a redirect to the HTML version of the users inbox, or return HTML directly. 36 | -------------------------------------------------------------------------------- /webfinger.md: -------------------------------------------------------------------------------- 1 | # Webfinger 2 | 3 | Webfinger is described quite well at the following https://webfinger.net/ and should be considered essential reading as it is used across the fediverse. 4 | 5 | --------------------------------------------------------------------------------