Merge branch 'main' of git.exozy.me:a/fuwuqi

This commit is contained in:
Anthony Wang 2023-01-18 14:48:32 -05:00
commit 1e65c7b7ef
No known key found for this signature in database
GPG key ID: 42A5B952E6DD8D38
6 changed files with 56 additions and 28 deletions

View file

@ -37,9 +37,7 @@ Enjoy your new "extremely hardcore" ActivityPub server!!! 🎉😎🚀🙃🥳
Since Fuwuqi's code is super duper easy to read and extend, the following features are left as an exercise to the reader: Since Fuwuqi's code is super duper easy to read and extend, the following features are left as an exercise to the reader:
- Multi-user support (hint: dynamically generate `.well-known/webfinger` instead of serving a static file) - Multi-user support (hint: dynamically generate `.well-known/webfinger` instead of serving a static file)
- Liking posts (hint: the implementation is very similar to `Follow`) - Deleting posts
- Announcing posts, AKA boosting
- Unfollowing, unliking, etc (hint: implement `collection_remove`)
- JSON-LD (hint: don't do it, your brain will thank you) - JSON-LD (hint: don't do it, your brain will thank you)
- Lots of pain - Lots of pain

View file

@ -5,11 +5,10 @@ from email.utils import formatdate
from requests import post from requests import post
from sys import argv from sys import argv
date = formatdate(usegmt=True)
with open(argv[1], 'rb') as f: with open(argv[1], 'rb') as f:
activity = f.read() activity = f.read()
date = formatdate(usegmt=True)
digester = hashes.Hash(hashes.SHA256()) digester = hashes.Hash(hashes.SHA256())
digester.update(activity) digester.update(activity)
digest = b64encode(digester.finalize()).decode() digest = b64encode(digester.finalize()).decode()

View file

@ -1,6 +1,6 @@
{ {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://0.exozy.me/users/test.outbox/follow", "id": "https://0.exozy.me/users/test.outbox#follow",
"type": "Follow", "type": "Follow",
"actor": "https://0.exozy.me/users/test.jsonld", "actor": "https://0.exozy.me/users/test.jsonld",
"object": "https://social.exozy.me/users/guest", "object": "https://social.exozy.me/users/guest",

View file

@ -15,26 +15,43 @@ domain = 'https://0.exozy.me'
def collection_append(file, item): def collection_append(file, item):
with open(file) as f: with open(file) as f:
collection = load(f) collection = load(f)
collection['totalItems'] += 1
collection['orderedItems'].append(item) collection['orderedItems'].append(item)
collection['totalItems'] += 1
with open(file, 'w') as f:
dump(collection, f)
def collection_pop(file, item):
with open(file) as f:
collection = load(f)
collection['orderedItems'].pop(item)
collection['totalItems'] -= 1
with open(file, 'w') as f: with open(file, 'w') as f:
dump(collection, f) dump(collection, f)
def iri_to_actor(iri): def iri_to_actor(iri):
if domain in iri: if domain in iri:
name = search(f'^{domain}/users/(.*?)#main-key$', iri).group(1) name = search(f'^{domain}/users/(.*?)$', iri.removesuffix('#main-key')).group(1)
actorfile = f'users/{name}' actorfile = f'users/{name}'
else: else:
actorfile = f'users/{quote_plus(iri)}' actorfile = f'users/{quote_plus(iri.removesuffix("#main-key"))}'
if not isfile(actorfile): if not isfile(actorfile):
with open(actorfile, 'w') as f: with open(actorfile, 'w') as f:
resp = get(iri.removesuffix('#main-key'), headers={'Accept': 'application/activity+json'}) resp = get(iri, headers={'Accept': 'application/activity+json'})
f.write(resp.text) f.write(resp.text)
with open(actorfile) as f: with open(actorfile) as f:
return load(f) return load(f)
def send(to, headers, body):
actor = iri_to_actor(to)
headers['Host'] = to.split('/')[2]
resp = post(actor['inbox'], headers=headers, data=body)
print(resp)
print(resp.text)
class fuwuqi(SimpleHTTPRequestHandler): class fuwuqi(SimpleHTTPRequestHandler):
def do_POST(self): def do_POST(self):
body = self.rfile.read(int(self.headers['Content-Length'])) body = self.rfile.read(int(self.headers['Content-Length']))
@ -72,10 +89,10 @@ class fuwuqi(SimpleHTTPRequestHandler):
# Make sure activity doer matches HTTP signature # Make sure activity doer matches HTTP signature
actor = keyid.removesuffix('#main-key') actor = keyid.removesuffix('#main-key')
if 'actor' in activity and activity['actor'] != actor: if ('actor' in activity and activity['actor'] != actor) or
self.send_response(401) ('attributedTo' in activity and activity['attributedTo'] != actor) or
return ('actor' in activity['object'] and activity['object']['actor'] != actor) or
if 'attributedTo' in activity and activity['attributedTo'] != actor: ('attributedTo' in activity['object'] and activity['object']['attributedTo'] != actor)
self.send_response(401) self.send_response(401)
return return
@ -85,25 +102,36 @@ class fuwuqi(SimpleHTTPRequestHandler):
elif self.path.endswith('outbox'): elif self.path.endswith('outbox'):
# C2S # C2S
collection_append(f'users/{username}.outbox', activity) collection_append(f'users/{username}.outbox', activity)
# Clients responsible for addressing activity
for to in activity['to']:
if 'followers' in to or to 'https://www.w3.org/ns/activitystreams#Public':
with open(f'users/{username}.followers') as f:
for follower in load(f)['orderedItems']:
send(follower, self.headers, body)
else:
send(to, self.headers, body)
# Process activity
if activity['type'] == 'Create': if activity['type'] == 'Create':
id = activity['id'].split('/')[-1] # Post
id = activity['object']['id'].split('/')[-1]
with open(f'users/{username}.statuses/{id}', 'w') as f: with open(f'users/{username}.statuses/{id}', 'w') as f:
dump(activity['object'], f) dump(activity['object'], f)
# Send to followers elif activity['type'] == 'Accept':
with open(f'users/{username}.following') as f: # Accept follow request
for followed in load(f)['orderedItems']: collection_append(f'users/{username}.followers', activity['object']['actor'])
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': elif activity['type'] == 'Follow':
object = iri_to_actor(activity['object']) # Follow request
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']) collection_append(f'users/{username}.following', activity['object'])
elif activity['type'] == 'Like':
# Like post
collection_append(f'users/{username}.liked', activity['object'])
elif activity['type'] == 'Undo':
if activity['object']['type'] == 'Follow':
# Unfollow request
collection_remove(f'users/{username}.following', activity['object']['object'])
elif activity['object']['type'] == 'Like':
# Unlike post
collection_remove(f'users/{username}.liked', activity['object']['object'])
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()

View file

@ -7,10 +7,12 @@
"type": "Person", "type": "Person",
"preferredUsername": "test", "preferredUsername": "test",
"name": "Billiam Wender", "name": "Billiam Wender",
"summary": "idk",
"inbox": "https://0.exozy.me/users/test.inbox", "inbox": "https://0.exozy.me/users/test.inbox",
"outbox": "https://0.exozy.me/users/test.outbox", "outbox": "https://0.exozy.me/users/test.outbox",
"followers": "https://0.exozy.me/users/test.followers", "followers": "https://0.exozy.me/users/test.followers",
"following": "https://0.exozy.me/users/test.following", "following": "https://0.exozy.me/users/test.following",
"liked": "https://0.exozy.me/users/test.liked",
"icon": { "icon": {
"type": "Image", "type": "Image",
"mediaType": "image/png", "mediaType": "image/png",

1
users/test.liked Normal file
View file

@ -0,0 +1 @@
{"@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "totalItems": 0, "orderedItems": []}