diff --git a/README.md b/README.md
index b70d965..cee60f6 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,9 @@
Fuwuqi (fúwùqì or 服务器 means "server" in Chinese) is a useless C2S ActivityPub server for "extremely hardcore" ActivityPub enthusiasts. Craft your own exquisite WebFinger response! Customize your actor object exactly like you want! Hack and extend the 100-line Python server code!
-If that sounds like a world of pain, close this webpage now... OK, got that out of the way. Time for some fun!
-
## Configuration
-First, clone this repo on your server.
+First, clone this repo on your server and your client device.
Now, generate an RSA keypair on your client device:
```bash
@@ -14,22 +12,24 @@ openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
```
-Rename `users/test.jsonld` to your username and pop it open in your favorite text editor. You should change `0.exozy.me` to match your server's domain name, `test` to your username, and the `publicKeyPem` field to the public key you just generated.
+On the server, rename `users/test.jsonld` to your username and pop it open in your favorite text editor. You should change `0.exozy.me` to match your server's domain name, `test` to your username, and the `publicKeyPem` field to the public key you just generated.
-Onward! Now open `.well-known/webfinger` in your editor, and modify it similarly.
+Onward! Now open `.well-known/webfinger` in your editor, and modify it similarly. Finally, open `server.py` and update the domain, obviously.
That wasn't so bad, was it? (sobbing sounds)
## Usage
-Alright, time for the real deal. Start up `python main.py` on your server. If you want to customize the port or whatever, just change the source code.
+Alright, time for the real deal. Start up `python server.py` on your server. If you want to customize the port or whatever, just change the source code.
-Now on your client device, open up your favorite C2S ActivityPub client... oh wait... there aren't any! Welp, you'll just have to settle for reading the [AP spec](https://www.w3.org/TR/activitypub/), writing some homemade JSON, and figuring out how `python client.py` works.
+Now on your client device, open up your favorite C2S ActivityPub client... oh wait... there aren't any! Welp, you'll just have to settle for reading the [AP spec](https://www.w3.org/TR/activitypub/), writing some homemade JSON, and figuring out how `python client.py` works. That's rough, buddy. You'll have to learn how to manually write `Note`s, manually accept follow requests, and more! You can find some examples in this repo. HTTP signatures are generated client-side, for no good reason other than it works. If you want to view your unread messages, just `curl` your inbox. Easy as that.
-Like and subscribe and enjoy your new "extremely hardcore" ActivityPub server!!! 🎉😎🚀🙃🥳
+Enjoy your new "extremely hardcore" ActivityPub server!!! 🎉😎🚀🙃🥳
## Resources
- https://www.w3.org/TR/activitypub/
- https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
- https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/
+- https://docs.joinmastodon.org/spec/
+- https://socialhub.activitypub.rocks/t/python-mastodon-server-post-with-http-signature/2757
diff --git a/accept.jsonld b/accept.jsonld
new file mode 100644
index 0000000..6af9bbb
--- /dev/null
+++ b/accept.jsonld
@@ -0,0 +1,12 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "type": "Accept",
+ "actor": "https://0.exozy.me/users/test.jsonld",
+ "object": {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://social.exozy.me/b363f127-6422-4566-b8f1-878aa33b0e1c",
+ "type": "Follow",
+ "actor": "https://social.exozy.me/users/a",
+ "object": "https://0.exozy.me/users/test.jsonld"
+ }
+}
diff --git a/activity.jsonld b/activity.jsonld
deleted file mode 100644
index ab5837d..0000000
--- a/activity.jsonld
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "@context": "https://www.w3.org/ns/activitystreams",
- "id": "https://0.exozy.me/users/test.outbox/hello-world2",
- "type": "Create",
- "actor": "https://0.exozy.me/users/test.jsonld",
- "object": {
- "id": "https://0.exozy.me/users/test.statuses/hello-world2",
- "type": "Note",
- "attributedTo": "https://0.exozy.me/users/test.jsonld",
- "inReplyTo": "https://social.exozy.me/@a/109707513227348721",
- "content": "Hello from fuwuqi! 2",
- "to": "https://www.w3.org/ns/activitystreams#Public"
- }
-}
diff --git a/client.py b/client.py
index 8cf4281..0a3305d 100644
--- a/client.py
+++ b/client.py
@@ -3,16 +3,17 @@ from cryptography.hazmat.primitives.asymmetric import padding
from base64 import b64encode
from email.utils import formatdate
from requests import post
+from sys import argv
date = formatdate(usegmt=True)
-with open('activity.jsonld', 'rb') as f:
+with open(argv[1], 'rb') as f:
activity = f.read()
digester = hashes.Hash(hashes.SHA256())
digester.update(activity)
digest = b64encode(digester.finalize()).decode()
-message = f'(request-target): post /users/a/inbox\nhost: social.exozy.me\ndate: {date}\ndigest: SHA-256={digest}'
+message = f'date: {date}\ndigest: SHA-256={digest}'
with open('private.pem', 'rb') as f:
privkey = serialization.load_pem_private_key(f.read(), None)
@@ -22,11 +23,9 @@ signature = b64encode(privkey.sign(
padding.PKCS1v15(),
hashes.SHA256()
)).decode()
-header = f'keyId="https://0.exozy.me/users/test.jsonld#main-key",headers="(request-target) host date digest",signature="{signature}"'
+header = f'keyId="https://0.exozy.me/users/test.jsonld#main-key",headers="date digest",signature="{signature}"'
-resp = post('http://localhost:4200/users/test.outbox', headers={
- '(request-target)': 'post /users/a/inbox',
- 'Host': 'social.exozy.me',
+resp = post('https://0.exozy.me/users/test.outbox', headers={
'Date': date,
'Digest': f'SHA-256={digest}',
'Signature': header,
diff --git a/create.jsonld b/create.jsonld
new file mode 100644
index 0000000..a9c8893
--- /dev/null
+++ b/create.jsonld
@@ -0,0 +1,14 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://0.exozy.me/users/test.outbox/hello-world2",
+ "type": "Create",
+ "actor": "https://0.exozy.me/users/test.jsonld",
+ "object": {
+ "id": "https://0.exozy.me/users/test.statuses/hello-world2",
+ "type": "Note",
+ "attributedTo": "https://0.exozy.me/users/test.jsonld",
+ "inReplyTo": "https://social.exozy.me/@a/109707513227348721",
+ "content": "Hello from fuwuqi! 2",
+ "to": "https://www.w3.org/ns/activitystreams#Public"
+ }
+}
diff --git a/follow.jsonld b/follow.jsonld
new file mode 100644
index 0000000..290ca70
--- /dev/null
+++ b/follow.jsonld
@@ -0,0 +1,7 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://0.exozy.me/users/test.outbox/follow",
+ "type": "Follow",
+ "actor": "https://0.exozy.me/users/test.jsonld",
+ "object": "https://social.exozy.me/users/a"
+}
diff --git a/private.pem b/private.pem
deleted file mode 100644
index 93c0375..0000000
--- a/private.pem
+++ /dev/null
@@ -1,28 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCc+fpJFDUlPCoU
-wUmrsGDExb/hX8dhKMc9u0ov/ePBc5OY4tTPjCvghvUjCIqmyC7PS7JfUuNt7epD
-OpoJm7mF2gaj64mxqfPrICgtjARN8gUiOpCvcWggZDFgkMQuSFI7yG1s4oqhGJUq
-yMPnHhBwSRmQsBp5/xZWHTywSoTd8Li4uY6VJq/JxswuZqH1R2GWMVxpVJDuqjH0
-cW8oXZ6LgGY3ZZ0wZnwk3bSdzQ7GhmkGkwCtGoXSSuMiSEDn/0K8hkRQs9AKFgvo
-ByYtLSP/JPETzZ4UwEG/J6DwgIxHNxsKg36FWe/dxv/v4HA12hCNmq3REb+GFulz
-xEq35g71AgMBAAECggEACqnGcS67ZMyfSn/4xP4QYhLRc89MSBLQ5KYeJirCd70l
-97riMY5i2qKARhbJ8wYNJqK3OqzIQIrqoLzQguTo/NPbI0kYNjvbvbXrqhsP8yAv
-bhb5W27s36ppWkHAjyj1gRRz2R4obu9bDqggT/Olso2cpsyiTSA2qwyFt1n7MueQ
-ag/lSYAAnjRfnxnGiBwy+wURxZH+Y9uBhb9E8MZ+W+sUc7l8RDUfYPN9d7NmxfdV
-x6x+jk+OLpYotj1v6aTfdwBCqpdt2ulMZexTCfjwAIU/XI2uhqaO1dzKI+Ssr/WY
-j8R7LXV94pI1ZXdwAJ/8aWGllAhj1hpnhacjnr7JAQKBgQDZPj5zJVk7TBoRxQVw
-VCfmGwPwAkfmGGqn9g41VlKtyG+JhWQ7aAs0gMFwq6QGDEBloAiXQw7z033ZSNTP
-p61QCePq6fLGEgdsfgjGotEbZ2XDZdB6fdzK+sKJLQtXr8FVrywyNmlX0bk8IRSs
-hnJXYco/VTLeDiQePO2bjm0UdQKBgQC4+0hR/se6AHgN9449ruHLSOCcC+Y6WQJ1
-BM+ydk7OnnfDmxzXQMbzri5kwDTBw+Y01avbRkpMZbHtWip9QWL96Kf8D6OUoX6d
-KJuheKsuJUKmXTPF8YyJiY+KXmx1yz7hu8X/SN955s4BAttIZGQvSIFpy+izzQWc
-FghcplvAgQKBgQCIkuIN37AOYFSPUU6PBMkkl11NWRG8bSM4Pq9GBuPpjvXX/f06
-f7lzo3J5E98FUlR1zzs3ZRgUX6RhorDvb1m81Mrtl3Bh51m1cjKwNhHB6aoHQo3j
-RBc3oJgGR0Q3Ny4TYRIm6yAk7ptGWwG1SLy/hKHyWOymvzsjq2gxgEPBNQKBgEyM
-6KvOBPdLVGNrS/jo01Yd7Z2GKxuAVEz61bzjys8ksylGmpPVob+cGGTnSa3aFP1O
-Y1VV7E9bUluIEcdN9NpgmovsKOTMRCpjcKxM1II/NyrDrTZANMmCHN3FH5tLpdUi
-sNhpXtoCksPGW9rEeNU8axnOIZmuwaCLWaCF07iBAoGAX494Slca2EnERTtcX9jq
-SM/srMTz+cm5zNA3npMj1eZ7zNglD5tZapc3f5OErrDdZz5wlmIo/eheQxb8TMQW
-F5goYHwpq6bghHlbAxK2I353s8Q8WriLTvAYouHfEqd14AS42OXD44CBwU0V5rRV
-MmlfOVBGqPVHLQK/tOebZIw=
------END PRIVATE KEY-----
diff --git a/public.pem b/public.pem
deleted file mode 100644
index d8c25bf..0000000
--- a/public.pem
+++ /dev/null
@@ -1,9 +0,0 @@
------BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnPn6SRQ1JTwqFMFJq7Bg
-xMW/4V/HYSjHPbtKL/3jwXOTmOLUz4wr4Ib1IwiKpsguz0uyX1Ljbe3qQzqaCZu5
-hdoGo+uJsanz6yAoLYwETfIFIjqQr3FoIGQxYJDELkhSO8htbOKKoRiVKsjD5x4Q
-cEkZkLAaef8WVh08sEqE3fC4uLmOlSavycbMLmah9UdhljFcaVSQ7qox9HFvKF2e
-i4BmN2WdMGZ8JN20nc0OxoZpBpMArRqF0krjIkhA5/9CvIZEULPQChYL6AcmLS0j
-/yTxE82eFMBBvyeg8ICMRzcbCoN+hVnv3cb/7+BwNdoQjZqt0RG/hhbpc8RKt+YO
-9QIDAQAB
------END PUBLIC KEY-----
diff --git a/server.py b/server.py
index dc8f1c8..86f4872 100644
--- a/server.py
+++ b/server.py
@@ -9,6 +9,9 @@ from os.path import isfile
from urllib.parse import quote_plus
+domain = 'https://0.exozy.me'
+
+
def collection_append(file, item):
with open(file) as f:
collection = load(f)
@@ -18,6 +21,20 @@ def collection_append(file, item):
dump(collection, f)
+def iri_to_actor(iri):
+ if domain in iri:
+ name = search(f'^{domain}/users/(.*?)#main-key$', iri).group(1)
+ actorfile = f'users/{name}'
+ else:
+ actorfile = f'users/{quote_plus(iri)}'
+ if not isfile(actorfile):
+ with open(actorfile, 'w') as f:
+ resp = get(iri, headers={'Accept': 'application/activity+json'})
+ f.write(resp.text)
+ with open(actorfile) as f:
+ return load(f)
+
+
class fuwuqi(SimpleHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers['Content-Length']))
@@ -26,21 +43,22 @@ class fuwuqi(SimpleHTTPRequestHandler):
print(self.headers)
print(self.path)
+ username = search('^/users/(.*)\.(in|out)box$', self.path).group(1)
+
# Get actor public key
keyid = search('keyId="(.*?)"', self.headers['Signature']).group(1)
- actorfile = f'users/{quote_plus(keyid)}'
- if not isfile(actorfile):
- with open(actorfile, 'w') as f:
- f.write(get(keyid).text)
- with open(actorfile) as f:
- pubkeypem = load(f)['publicKey']['publicKeyPem'].encode('utf8')
- pubkey = serialization.load_pem_public_key(pubkeypem, None)
+ actor = iri_to_actor(keyid)
+ pubkeypem = actor['publicKey']['publicKeyPem'].encode('utf8')
+ pubkey = serialization.load_pem_public_key(pubkeypem, None)
# Assemble headers
headers = search('headers="(.*?)"', self.headers['Signature']).group(1)
message = ''
for header in headers.split():
- headerval = self.headers[header]
+ if header == '(request-target)':
+ headerval = f'post {self.path}'
+ else:
+ headerval = self.headers[header]
message += f'{header}: {headerval}\n'
# Verify HTTP signature
@@ -61,27 +79,34 @@ class fuwuqi(SimpleHTTPRequestHandler):
self.send_response(401)
return
- username = search('^/users/(.*)\.(in|out)box$', self.path).group(1)
if self.path.endswith('inbox'):
# S2S
- id = activity['id'].split('/')[-1]
- with open(f'users/{username}/{id}', 'w') as f:
- dump(activity, f)
+ collection_append(f'users/{username}.inbox', activity)
elif self.path.endswith('outbox'):
# C2S
collection_append(f'users/{username}.outbox', activity)
-
if activity['type'] == 'Create':
id = activity['id'].split('/')[-1]
with open(f'users/{username}.statuses/{id}', 'w') as f:
dump(activity['object'], f)
- print(self.headers)
- print(body)
- resp = post('https://social.exozy.me/users/a/inbox', headers=self.headers, data=body)
- print(resp)
- print(resp.text)
+ # Send to followers
+ with open(f'users/{username}.following') as f:
+ for followed in load(f)['orderedItems']:
+ actor = iri_to_actor(followed)
+ self.headers['Host'] = followed.split('/')[2]
+ resp = post(actor['inbox'], headers=self.headers, data=body)
+ print(resp)
+ print(resp.text)
+ elif activity['type'] == 'Follow':
+ object = iri_to_actor(activity['object'])
+ self.headers['Host'] = activity['object'].split('/')[2]
+ resp = post(object['inbox'], headers=self.headers, data=body)
+ print(resp)
+ print(resp.text)
+ collection_append(f'users/{username}.following', activity['object'])
self.send_response(200)
+ self.end_headers()
ThreadingHTTPServer(('localhost', 4200), fuwuqi).serve_forever()
diff --git a/users/https%3A%2F%2F0.exozy.me%2Fusers%2Ftest.jsonld%23main-key b/users/https%3A%2F%2F0.exozy.me%2Fusers%2Ftest.jsonld%23main-key
deleted file mode 100644
index 82cccc2..0000000
--- a/users/https%3A%2F%2F0.exozy.me%2Fusers%2Ftest.jsonld%23main-key
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "@context": [
- "https://www.w3.org/ns/activitystreams",
- "https://w3id.org/security/v1"
- ],
- "id": "https://0.exozy.me/users/test.jsonld",
- "type": "Person",
- "preferredUsername": "test",
- "name": "Billiam Wender",
- "inbox": "https://0.exozy.me/users/test.inbox",
- "outbox": "https://0.exozy.me/users/test.outbox",
- "followers": "https://0.exozy.me/users/test.followers",
- "following": "https://0.exozy.me/users/test.following",
- "icon": {
- "type": "Image",
- "mediaType": "image/png",
- "url": "https://0.exozy.me/users/test.png"
- },
- "publicKey": {
- "id": "https://0.exozy.me/users/test.jsonld#main-key",
- "owner": "https://0.exozy.me/users/test.jsonld",
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnPn6SRQ1JTwqFMFJq7Bg\nxMW/4V/HYSjHPbtKL/3jwXOTmOLUz4wr4Ib1IwiKpsguz0uyX1Ljbe3qQzqaCZu5\nhdoGo+uJsanz6yAoLYwETfIFIjqQr3FoIGQxYJDELkhSO8htbOKKoRiVKsjD5x4Q\ncEkZkLAaef8WVh08sEqE3fC4uLmOlSavycbMLmah9UdhljFcaVSQ7qox9HFvKF2e\ni4BmN2WdMGZ8JN20nc0OxoZpBpMArRqF0krjIkhA5/9CvIZEULPQChYL6AcmLS0j\n/yTxE82eFMBBvyeg8ICMRzcbCoN+hVnv3cb/7+BwNdoQjZqt0RG/hhbpc8RKt+YO\n9QIDAQAB\n-----END PUBLIC KEY-----\n"
- }
-}
diff --git a/users/https%3A%2F%2Fsocial.exozy.me%2Fusers%2Fa b/users/https%3A%2F%2Fsocial.exozy.me%2Fusers%2Fa
new file mode 100644
index 0000000..89442bd
--- /dev/null
+++ b/users/https%3A%2F%2Fsocial.exozy.me%2Fusers%2Fa
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","toot":"http://joinmastodon.org/ns#","featured":{"@id":"toot:featured","@type":"@id"},"featuredTags":{"@id":"toot:featuredTags","@type":"@id"},"alsoKnownAs":{"@id":"as:alsoKnownAs","@type":"@id"},"movedTo":{"@id":"as:movedTo","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","discoverable":"toot:discoverable","Device":"toot:Device","Ed25519Signature":"toot:Ed25519Signature","Ed25519Key":"toot:Ed25519Key","Curve25519Key":"toot:Curve25519Key","EncryptedMessage":"toot:EncryptedMessage","publicKeyBase64":"toot:publicKeyBase64","deviceId":"toot:deviceId","claim":{"@type":"@id","@id":"toot:claim"},"fingerprintKey":{"@type":"@id","@id":"toot:fingerprintKey"},"identityKey":{"@type":"@id","@id":"toot:identityKey"},"devices":{"@type":"@id","@id":"toot:devices"},"messageFranking":"toot:messageFranking","messageType":"toot:messageType","cipherText":"toot:cipherText","suspended":"toot:suspended","focalPoint":{"@container":"@list","@id":"toot:focalPoint"}}],"id":"https://social.exozy.me/users/a","type":"Person","following":"https://social.exozy.me/users/a/following","followers":"https://social.exozy.me/users/a/followers","inbox":"https://social.exozy.me/users/a/inbox","outbox":"https://social.exozy.me/users/a/outbox","featured":"https://social.exozy.me/users/a/collections/featured","featuredTags":"https://social.exozy.me/users/a/collections/tags","preferredUsername":"a","name":"","summary":"\u003cp\u003e(+ (My username) (Ordinal n) (! off) (Element 39) (Wide area network) (Prefix for billion))\u003c/p\u003e\u003cp\u003eBoosts interesting things and sometimes transcribes special patterns generated by my biological neural networks into UTF-8 encoded strings.\u003c/p\u003e\u003cp\u003eCurrently oscillating between hacking ActivityPub federation into Forgejo/Gitea, pondering about the ForgeFed spec, wasting time, and sleeping (with 1/3 probability!).\u003c/p\u003e\u003cp\u003eWill happily answer any questions about forge federation.\u003c/p\u003e","url":"https://social.exozy.me/@a","manuallyApprovesFollowers":false,"discoverable":true,"published":"2022-09-07T00:00:00Z","devices":"https://social.exozy.me/users/a/collections/devices","alsoKnownAs":["https://social.exozy.me/users/ta180m"],"publicKey":{"id":"https://social.exozy.me/users/a#main-key","owner":"https://social.exozy.me/users/a","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrHXYuP4RKjac7h0woPA\nmBeETGogZ2IPUbDhA4DliUUHbUGYIK3XeIA4iywpnTbuHxE7L2PqMYNvfYMjQ+HS\nlJfSgYLB5mFxvzNcKqaUfAZazx3RzHLxftxt4DxWhEtS+vVd4RsxU2uvCOU8nN7o\nvk40GaYrgAms/7sapAjqbn6ngclVtVOBm0QhCG9cfg4QZoIIp98wpj/7kWZHxl8Y\nKJKN4G66FP+WAPIiLO/EmBqB9jcTQM2UMIGol4+616zFbmrow4KFCxZhiap+doP2\n8SjZSlA4Fhlk4qtHvQRjPlx2u/gvyUc7gQoa3PTb64rdDw1ahGvLTQKB6uVAXpDN\n2wIDAQAB\n-----END PUBLIC KEY-----\n"},"tag":[],"attachment":[{"type":"PropertyValue","name":"Website","value":"\u003ca href=\"https://a.exozy.me\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003ea.exozy.me\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"Code","value":"\u003ca href=\"https://git.exozy.me/a\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003egit.exozy.me/a\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"exozyme","value":"\u003ca href=\"https://exozy.me\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003eexozy.me\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"Also me","value":"\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.mit.edu/@xy\" class=\"u-url mention\"\u003e@\u003cspan\u003exy@mastodon.mit.edu\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e"}],"endpoints":{"sharedInbox":"https://social.exozy.me/inbox"},"icon":{"type":"Image","mediaType":"image/gif","url":"https://social.exozy.me/system/accounts/avatars/108/958/469/936/988/491/original/c16834437aa85a2a.gif"},"image":{"type":"Image","mediaType":"image/jpeg","url":"https://social.exozy.me/system/accounts/headers/108/958/469/936/988/491/original/521f64b30b55553d.jpg"}}
\ No newline at end of file
diff --git a/users/https%3A%2F%2Fsocial.exozy.me%2Fusers%2Fa%23main-key b/users/https%3A%2F%2Fsocial.exozy.me%2Fusers%2Fa%23main-key
index 6fe1753..89442bd 100644
--- a/users/https%3A%2F%2Fsocial.exozy.me%2Fusers%2Fa%23main-key
+++ b/users/https%3A%2F%2Fsocial.exozy.me%2Fusers%2Fa%23main-key
@@ -1,98 +1 @@
-
-
-