├── LICENSE
├── README.md
├── example
├── README.md
├── config.py
├── follow_user.py
├── requirements.txt
└── server.py
└── title.png
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Timothy
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 |
2 |
3 |
4 |
5 | # ActivityPub Tutorial
6 |
7 | ## Requirements
8 | * You need a domain name
9 | * You need an HTTPS certificate for this domain name
10 | * Python3
11 | * Python3 packages: flask, cryptography, requests
12 |
13 | ## Short method
14 | 1. [Read the ActivityPub overview](#1-read-the-activitypub-overview)
15 | 2. [Create an endpoint for yourself](#2-create-an-endpoint-for-yourself)
16 | 3. [Extend this endpoint for real servers](#3-extend-this-endpoint-for-real-servers)
17 | 4. [Create a Webfinger endpoint for yourself](#4-create-a-webfinger-endpoint-for-yourself)
18 | 5. [Create an inbox endpoint for yourself](#5-create-an-inbox-endpoint-for-yourself)
19 | 6. [Follow a user from an instance to start receiving activities on your instance](#6-follow-a-user-from-an-instance-to-start-receiving-activities-on-your-instance)
20 |
21 | ## Long method
22 | We'll run with some assumptions. Your domain name is `example.com`, your user name will be `zampano`.
23 |
24 | ### 1. Read the ActivityPub overview
25 | [https://www.w3.org/TR/activitypub/#Overview](https://www.w3.org/TR/activitypub/#Overview)
26 |
27 | ### 2. Create an endpoint for yourself
28 | [https://www.w3.org/TR/activitypub/#actors](https://www.w3.org/TR/activitypub/#actors)
29 |
30 | ActivityStreams expects that we define a @context, id, type, and name property.
31 | ActivityPub expects that we define an inbox and outbox property.
32 |
33 | ```python
34 | @app.route('/users/')
35 | def user(username):
36 | if username != "zampano":
37 | abort(404)
38 |
39 | response = make_response({
40 | "@context": "https://www.w3.org/ns/activitystreams",
41 | "id": "https://example.com/users/zampano",
42 | "inbox": "https://example.com/users/zampano/inbox",
43 | "outbox": "https://example.com/users/zampano/outbox",
44 | "type": "Person",
45 | "name": "Zampano",
46 | })
47 |
48 | # Servers may discard the result if you do not set the appropriate content type
49 | response.headers['Content-Type'] = 'application/activity+json'
50 |
51 | return response
52 | ```
53 |
54 | ### 3. Extend this endpoint for real servers
55 |
56 | This would be okay and meets the core specification, but to interact with Mastodon we need to add the preferredUsername attribute (from ActivityPub) and we need to add the publicKey property (from Linked Data Proofs).
57 |
58 | #### Generate public and private keys
59 | ```sh
60 | openssl genrsa -out private.pem 2048
61 | openssl rsa -in private.pem -outform PEM -pubout -out public.pem
62 | ```
63 |
64 | or in Python
65 | ```python
66 | from cryptography.hazmat.primitives import serialization as crypto_serialization
67 | from cryptography.hazmat.primitives.asymmetric import rsa
68 | from cryptography.hazmat.backends import default_backend as crypto_default_backend
69 |
70 | key = rsa.generate_private_key(
71 | backend=crypto_default_backend(),
72 | public_exponent=65537,
73 | key_size=2048
74 | )
75 |
76 | private_key = key.private_bytes(
77 | crypto_serialization.Encoding.PEM,
78 | crypto_serialization.PrivateFormat.PKCS8,
79 | crypto_serialization.NoEncryption())
80 |
81 | public_key = key.public_key().public_bytes(
82 | crypto_serialization.Encoding.PEM,
83 | crypto_serialization.PublicFormat.SubjectPublicKeyInfo
84 | )
85 | ```
86 |
87 | #### Modify user endpoint
88 |
89 | ```python
90 | @app.route('/users/')
91 | def user(username):
92 | if username != "zampano":
93 | abort(404)
94 |
95 | public_key = b'' # retrieve from file/database
96 |
97 | response = make_response({
98 | "@context": [
99 | "https://www.w3.org/ns/activitystreams",
100 | "https://w3id.org/security/v1",
101 | ],
102 | "id": "https://example.com/users/zampano",
103 | "inbox": "https://example.com/users/zampano/inbox",
104 | "outbox": "https://example.com/users/zampano/outbox",
105 | "type": "Person",
106 | "name": "Zampano",
107 | "preferredUsername": "zampano",
108 | "publicKey": {
109 | "id": "https://example.com/users/zampano#main-key",
110 | "id": "https://example.com/users/zampano",
111 | "publicKeyPem": public_key
112 | }
113 | })
114 |
115 | # Servers may discard the result if you do not set the appropriate content type
116 | response.headers['Content-Type'] = 'application/activity+json'
117 |
118 | return response
119 | ```
120 |
121 |
122 | ### 4. Create a Webfinger endpoint for yourself
123 |
124 | "Web finger is used to discover information about people or other entities on the Internet that are identified by a URI."
125 | Some ActivityPub servers, like Mastodon, will use Webfinger to find the location of the Actor record we've been creating.
126 |
127 | ```python
128 | from flask import request, make_response
129 |
130 | # ...
131 |
132 | @app.route('/.well-known/webfinger')
133 | def webfinger():
134 | resource = request.args.get('resource')
135 |
136 | if resource != "acct:zampano@example.com":
137 | abort(404)
138 |
139 | response = make_response({
140 | "subject": "acct:zampano@example.com",
141 | "links": [
142 | {
143 | "rel": "self",
144 | "type": "application/activity+json",
145 | "href": "https://example.com/users/zampano"
146 | }
147 | ]
148 | })
149 |
150 | # Servers may discard the result if you do not set the appropriate content type
151 | response.headers['Content-Type'] = 'application/jrd+json'
152 |
153 | return response
154 | ```
155 |
156 | ### 5. Create an inbox endpoint for yourself
157 |
158 | We've defined an inbox and outbox property in our Person record.
159 | ```
160 | ...
161 | "inbox": "https://example.com/users/zampano/inbox",
162 | "outbox": "https://example.com/users/zampano/outbox",
163 | ...
164 | ```
165 |
166 | We will want to define the outbox later for the client-to-server interactions, but for now we can get away with just the inbox.
167 |
168 | ```python
169 | @app.route('/users//inbox', methods=['POST'])
170 | def user_inbox(username):
171 | if username != "zampano":
172 | abort(404)
173 |
174 | app.logger.info(request.headers)
175 | app.logger.info(request.data)
176 |
177 | return Response("", status=202)
178 | ```
179 |
180 | ### 6. Follow a user from an instance to start receiving activities on your instance
181 | You could feasibly follow any ActivityPub Actor now but I recommend testing with an account you control on a Mastodon instance, or with a bot account.
182 |
183 | Let's assume you're sending a follow request to the user 'truant' at the Mastodon instance 'exampletwo.com'.
184 |
185 | ```python
186 | from cryptography.hazmat.backends import default_backend as crypto_default_backend
187 | from cryptography.hazmat.primitives import serialization as crypto_serialization
188 | from cryptography.hazmat.primitives import hashes
189 | from cryptography.hazmat.primitives.asymmetric import padding
190 |
191 | from urllib.parse import urlparse
192 | import base64
193 | import datetime
194 | import requests
195 |
196 |
197 | recipient_url = "https://exampletwo.com/users/truant"
198 | recipient_inbox = "https://exampletwo.com/users/truant/inbox"
199 |
200 | sender_url = "https://example.com/users/zampano"
201 | sender_key = "https://example.com/users/zampano#main-key"
202 |
203 | activity_id = "https://example.com/users/zampano/follows/test"
204 |
205 |
206 | # The following is to sign the HTTP request as defined in HTTP Signatures.
207 | private_key_text = b'' # load from file
208 |
209 | private_key = crypto_serialization.load_pem_private_key(
210 | private_key_text,
211 | password=None,
212 | backend=crypto_default_backend()
213 | )
214 |
215 | current_date = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
216 |
217 | recipient_parsed = urlparse(recipient_inbox)
218 | recipient_host = recipient_parsed.netloc
219 | recipient_path = recipient_parsed.path
220 |
221 | signature_text = b'(request-target): post %s\nhost: %s\ndate: %s' % recipient_path.encode('utf-8'), recipient_host.encode('utf-8'), date.encode('utf-8')
222 |
223 | raw_signature = private_key.sign(
224 | signature_text,
225 | padding.PKCS1v15(),
226 | hashes.SHA256()
227 | )
228 |
229 | signature_header = 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date",signature="%s"' % sender_key, base64.b64encode(raw_signature).decode('utf-8')
230 |
231 | headers = {
232 | 'Date': date,
233 | 'Content-Type': 'application/activity+json',
234 | 'Host': recipient_host,
235 | 'Signature': signature_header
236 | }
237 |
238 | # Now that the header is set up, we will construct the message
239 | follow_request_message = {
240 | "@context": "https://www.w3.org/ns/activitystreams",
241 | "id": activity_id,
242 | "type": "Follow",
243 | "actor": sender_url,
244 | "object": recipient_url
245 | }
246 |
247 | r = requests.post(recipient_inbox, headers=headers, json=follow_request_message)
248 | ```
249 |
250 | ### 7. Mastodon now requires the Digest header
251 | Thanks @Yoxem for the info.
252 |
253 | ```python
254 | import json
255 | import hashlib
256 |
257 | follow_request_message = { ... } # as above
258 | follow_request_json = json.dumps(follow_request_message)
259 | digest = base64.b64encode(hashlib.sha256(follow_request_json.encode('utf-8')).digest())
260 |
261 | # signature information is now
262 | signature_text = b'(request-target): post %s\ndigest: SHA-256=%s\nhost: %s\ndate: %s' % (recipient_path.encode('utf-8'), digest, recipient_host.encode('utf-8'), current_date.encode('utf-8'))
263 |
264 | raw_signature = private_key.sign(
265 | signature_text,
266 | padding.PKCS1v15(),
267 | hashes.SHA256()
268 | )
269 |
270 | signature_header = 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) digest host date",signature="%s"' % sender_key, base64.b64encode(raw_signature).decode('utf-8')
271 |
272 | headers = {
273 | 'Date': current_date,
274 | 'Content-Type': 'application/activity+json',
275 | 'Host': recipient_host,
276 | 'Digest': "SHA-256="+digest.decode('utf-8'),
277 | 'Signature': signature_header
278 | }
279 | ```
280 |
281 | ## Standards
282 | * [ActivityPub](https://www.w3.org/TR/activitypub/)
283 | * [ActivityStreams 2.0](https://www.w3.org/TR/activitystreams-core/)
284 | * [JSON-LD](https://www.w3.org/TR/json-ld/)
285 | * [HTTP signatures (draft 12)](https://tools.ietf.org/id/draft-cavage-http-signatures-12.html)
286 | * [Linked Data Proofs](https://w3c-ccg.github.io/ld-proofs/) (previously Linked Data Signatures)
287 | * [Webfinger](https://tools.ietf.org/html/rfc7033)
288 |
289 |
290 | ## Resources
291 | * [How to implement a basic ActivityPub server](https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/)
292 | * [How to make friends and verify requests](https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/)
293 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # ActivityPub Example
2 |
3 | Here is a less wordy example which will let you see some results immediately.
4 |
5 | 1. Set up a server using Let's Encrypt to get your HTTPS certificates.
6 | 2. Turn off any web servers you may have used, e.g. nginx/apache/caddy
7 | 3. Generate a public/private key pair for your user
8 | ```sh
9 | openssl genrsa -out private.pem 2048
10 | openssl rsa -in private.pem -outform PEM -pubout -out public.pem
11 | ```
12 | 4. Set up Python environment
13 | ```sh
14 | python3 -m venv env
15 | source env/bin/activate
16 | pip install -r requirements.txt
17 | ```
18 | 5. Enter your own details in config.py
19 | 6. Run the server in one shell `python server.py`
20 | a. This may need to run as root as it binds port 443
21 | b. Confirm by searching your user on Mastodon, e.g. `@user@host.com` and note what response you receive
22 | 7. Run `python follow_user.py` and `python follow_user.py --unfollow`
23 | a. The server must be running for Mastodon to send an accept message.
24 |
--------------------------------------------------------------------------------
/example/config.py:
--------------------------------------------------------------------------------
1 | # Limit yourself to one test user otherwise every username will be valid
2 | # and Mastodon will cache these until you provide it with a Delete action
3 | HOSTNAME = "example.com"
4 | USER = "johnny"
5 |
6 | # Ideally use a user account you own on a working ActivityPub instance
7 | TEST_HOSTNAME = "example.com" # mastodon.social
8 | TEST_USER = "zampano"
9 |
--------------------------------------------------------------------------------
/example/follow_user.py:
--------------------------------------------------------------------------------
1 | from cryptography.hazmat.backends import default_backend as crypto_default_backend
2 | from cryptography.hazmat.primitives import serialization as crypto_serialization
3 | from cryptography.hazmat.primitives import hashes
4 | from cryptography.hazmat.primitives.asymmetric import padding
5 |
6 | from urllib.parse import urlparse
7 | import base64
8 | import datetime
9 | import requests
10 | import json
11 | import hashlib
12 | import sys
13 | from config import HOSTNAME, USER, TEST_HOSTNAME, TEST_USER
14 |
15 |
16 | OWNED_URI = f"https://{HOSTNAME}"
17 | OWNED_USER = USER
18 |
19 | sender_url = f"{OWNED_URI}/users/{OWNED_USER}"
20 | sender_key = f"{OWNED_URI}/users/{OWNED_USER}#main-key"
21 | # NOTE: This id should be unique for each action
22 | activity_id = f"{OWNED_URI}/users/{OWNED_USER}/follows/test"
23 |
24 | TEST_URI = f"https://{TEST_HOSTNAME}"
25 |
26 |
27 | def get_resource_from_webfinger(uri, resource):
28 | print("Getting recipient url from .well-known/webfinger")
29 | webfinger = requests.get(
30 | f"{uri}/.well-known/webfinger?resource={resource}",
31 | headers={"Accept": "application/jrd+json, application/json"},
32 | )
33 | return json.loads(webfinger.content)
34 |
35 |
36 | def get_canonical_from_webfinger(uri, resource):
37 | webfinger_json = get_resource_from_webfinger(uri, resource)
38 |
39 | if "links" not in webfinger_json:
40 | print("No links in webfinger")
41 | return None
42 |
43 | self_uri = [
44 | link["href"] for link in webfinger_json["links"] if link["rel"] == "self"
45 | ]
46 | if len(self_uri) == 0:
47 | print("No self uri")
48 | return None
49 |
50 | return self_uri[0]
51 |
52 |
53 | def get_inbox_from_canonical_user(uri, resource):
54 | recipient_url = get_canonical_from_webfinger(uri, resource)
55 |
56 | user = requests.get(
57 | recipient_url, headers={"Accept": "application/activity+json, application/json"}
58 | )
59 | user_json = json.loads(user.content)
60 |
61 | if "inbox" not in user_json:
62 | print("No inbox")
63 | return None
64 |
65 | return user_json["inbox"]
66 |
67 |
68 | def sign(text):
69 | # The following is to sign the HTTP request as defined in HTTP Signatures.
70 | private_key_text = open("private.pem", "rb").read() # load from file
71 |
72 | private_key = crypto_serialization.load_pem_private_key(
73 | private_key_text, password=None, backend=crypto_default_backend()
74 | )
75 | return private_key.sign(text, padding.PKCS1v15(), hashes.SHA256())
76 |
77 |
78 | def follow():
79 | recipient_url = get_canonical_from_webfinger(
80 | TEST_URI, f"acct:{TEST_USER}@{TEST_HOSTNAME}"
81 | )
82 | recipient_inbox = get_inbox_from_canonical_user(
83 | TEST_URI, f"acct:{TEST_USER}@{TEST_HOSTNAME}"
84 | )
85 |
86 | print(f"Sending follow request from {sender_url} to {recipient_inbox}")
87 |
88 | recipient_parsed = urlparse(recipient_inbox)
89 | recipient_host = recipient_parsed.netloc
90 | recipient_path = recipient_parsed.path
91 |
92 | follow_request_message = {
93 | "@context": "https://www.w3.org/ns/activitystreams",
94 | "id": activity_id,
95 | "type": "Follow",
96 | "actor": sender_url,
97 | "object": recipient_url,
98 | }
99 |
100 | digest = base64.b64encode(
101 | hashlib.sha256(json.dumps(follow_request_message).encode("utf-8")).digest()
102 | )
103 |
104 | current_date = datetime.datetime.now(datetime.timezone.utc).strftime(
105 | "%a, %d %b %Y %H:%M:%S GMT"
106 | )
107 |
108 | # signature_text = generate_signing_text
109 | signature_text: bytes = b"(request-target): post " + recipient_path.encode("utf-8")
110 | signature_text += b"\nhost: " + recipient_host.encode("utf-8")
111 | signature_text += b"\ndate: " + current_date.encode("utf-8")
112 | signature_text += b"\ndigest: SHA-256=" + digest
113 |
114 | raw_signature = sign(signature_text)
115 |
116 | signature_text_b64 = base64.b64encode(raw_signature).decode("utf-8")
117 | signature_header = (
118 | 'keyId="'
119 | + sender_key
120 | + '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="'
121 | + signature_text_b64
122 | + '"'
123 | )
124 |
125 | headers = {
126 | "Date": current_date,
127 | "Content-Type": "application/activity+json",
128 | "Host": recipient_host,
129 | "Digest": "SHA-256=" + digest.decode("utf-8"),
130 | "Signature": signature_header,
131 | }
132 | print(headers)
133 |
134 | # Now that the header is set up, we will construct the message
135 | r = requests.post(recipient_inbox, headers=headers, json=follow_request_message)
136 | print(r)
137 | print(r.content)
138 |
139 |
140 | def unfollow():
141 | recipient_url = get_canonical_from_webfinger(
142 | TEST_URI, f"acct:{TEST_USER}@{TEST_HOSTNAME}"
143 | )
144 | recipient_inbox = get_inbox_from_canonical_user(
145 | TEST_URI, f"acct:{TEST_USER}@{TEST_HOSTNAME}"
146 | )
147 |
148 | print(f"Sending unfollow request from {sender_url} to {recipient_inbox}")
149 |
150 | recipient_parsed = urlparse(recipient_inbox)
151 | recipient_host = recipient_parsed.netloc
152 | recipient_path = recipient_parsed.path
153 |
154 | follow_request_message = {
155 | "id": activity_id,
156 | "type": "Follow",
157 | "actor": sender_url,
158 | "object": recipient_url,
159 | }
160 |
161 | unfollow_request_message = {
162 | "@context": "https://www.w3.org/ns/activitystreams",
163 | "id": f"{activity_id}/undo",
164 | "type": "Undo",
165 | "actor": sender_url,
166 | "object": follow_request_message,
167 | }
168 |
169 | digest = base64.b64encode(
170 | hashlib.sha256(json.dumps(unfollow_request_message).encode("utf-8")).digest()
171 | )
172 |
173 | current_date = datetime.datetime.now(datetime.timezone.utc).strftime(
174 | "%a, %d %b %Y %H:%M:%S GMT"
175 | )
176 |
177 | # signature_text = generate_signing_text
178 | signature_text: bytes = b"(request-target): post " + recipient_path.encode("utf-8")
179 | signature_text += b"\nhost: " + recipient_host.encode("utf-8")
180 | signature_text += b"\ndate: " + current_date.encode("utf-8")
181 | signature_text += b"\ndigest: SHA-256=" + digest
182 |
183 | raw_signature = sign(signature_text)
184 |
185 | signature_text_b64 = base64.b64encode(raw_signature).decode("utf-8")
186 | signature_header = (
187 | 'keyId="'
188 | + sender_key
189 | + '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="'
190 | + signature_text_b64
191 | + '"'
192 | )
193 |
194 | headers = {
195 | "Date": current_date,
196 | "Content-Type": "application/activity+json",
197 | "Host": recipient_host,
198 | "Digest": "SHA-256=" + digest.decode("utf-8"),
199 | "Signature": signature_header,
200 | }
201 | print(headers)
202 |
203 | # Now that the header is set up, we will construct the message
204 | r = requests.post(recipient_inbox, headers=headers, json=unfollow_request_message)
205 | print(r)
206 | print(r.content)
207 |
208 |
209 | if __name__ == "__main__":
210 | command = "follow"
211 | if len(sys.argv) > 1:
212 | if sys.argv[1] == "--unfollow":
213 | command = "unfollow"
214 |
215 | if command == "follow":
216 | follow()
217 | elif command == "unfollow":
218 | unfollow()
219 | else:
220 | print("Command not handled")
221 |
--------------------------------------------------------------------------------
/example/requirements.txt:
--------------------------------------------------------------------------------
1 | cryptography==38.0.1
2 | requests==2.28.1
3 | flask==3.1.0
4 |
--------------------------------------------------------------------------------
/example/server.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request, make_response, abort, Response
2 | import ssl
3 | from config import HOSTNAME, USER
4 |
5 | URI = f"https://{HOSTNAME}"
6 |
7 | context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
8 | context.load_cert_chain(
9 | f"/etc/letsencrypt/live/{HOSTNAME}/fullchain.pem",
10 | f"/etc/letsencrypt/live/{HOSTNAME}/privkey.pem",
11 | )
12 |
13 | app = Flask(__name__)
14 |
15 |
16 | @app.route("/users/")
17 | def user(username):
18 | print(f"Received GET request for /users/{username}")
19 | print(request.headers)
20 |
21 | if username != USER:
22 | abort(404)
23 |
24 | public_key = open("public.pem", "r").read()
25 |
26 | response = make_response(
27 | {
28 | "@context": [
29 | "https://www.w3.org/ns/activitystreams",
30 | "https://w3id.org/security/v1",
31 | ],
32 | "id": f"{URI}/users/{USER}",
33 | "inbox": f"{URI}/users/{USER}/inbox",
34 | "outbox": f"{URI}/users/{USER}/outbox",
35 | "type": "Person",
36 | "name": USER.title(),
37 | "preferredUsername": USER,
38 | "publicKey": {
39 | "id": f"{URI}/users/{USER}#main-key",
40 | "owner": f"{URI}/users/{USER}",
41 | "publicKeyPem": public_key,
42 | },
43 | }
44 | )
45 |
46 | # Servers may discard the result if you do not set the appropriate content type
47 | response.headers["Content-Type"] = "application/activity+json"
48 |
49 | return response
50 |
51 |
52 | @app.route("/users//inbox", methods=["POST"])
53 | def user_inbox(username):
54 | print(f"Received POST request for /users/{username}/inbox")
55 | print(request.headers)
56 | print(request.data)
57 |
58 | if username != USER:
59 | abort(404)
60 |
61 | # Accept any message sent to our inbox while testing
62 | return Response("", status=202)
63 |
64 |
65 | @app.route("/.well-known/webfinger")
66 | def webfinger():
67 | resource = request.args.get("resource")
68 |
69 | if resource != f"acct:{USER}@{HOSTNAME}":
70 | abort(404)
71 |
72 | response = make_response(
73 | {
74 | "subject": f"acct:{USER}@{HOSTNAME}",
75 | "links": [
76 | {
77 | "rel": "self",
78 | "type": "application/activity+json",
79 | "href": f"{URI}/users/{USER}",
80 | }
81 | ],
82 | }
83 | )
84 |
85 | # Servers may discard the result if you do not set the appropriate content type
86 | response.headers["Content-Type"] = "application/jrd+json"
87 |
88 | return response
89 |
90 |
91 | @app.route("/", defaults={"path": ""})
92 | @app.route("/")
93 | def catch_all(path):
94 | print(path)
95 | print(request.headers)
96 | print(request.data)
97 |
98 | return ""
99 |
100 |
101 | if __name__ == "__main__":
102 | app.run(host="0.0.0.0", port=443, ssl_context=context)
103 |
--------------------------------------------------------------------------------
/title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timmot/activity-pub-tutorial/9279f22cbefd7013c0d9d44b91f1a3860048b2d0/title.png
--------------------------------------------------------------------------------