Implement some more server features

This commit is contained in:
Anthony Wang 2023-01-18 19:48:11 +00:00
parent 60bf76a75e
commit 9145dc7a8e
No known key found for this signature in database
GPG key ID: 42A5B952E6DD8D38
4 changed files with 54 additions and 25 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:
- 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`)
- Announcing posts, AKA boosting
- Unfollowing, unliking, etc (hint: implement `collection_remove`)
- Deleting posts
- JSON-LD (hint: don't do it, your brain will thank you)
- Lots of pain

View file

@ -15,26 +15,43 @@ domain = 'https://0.exozy.me'
def collection_append(file, item):
with open(file) as f:
collection = load(f)
collection['totalItems'] += 1
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:
dump(collection, f)
def iri_to_actor(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}'
else:
actorfile = f'users/{quote_plus(iri)}'
actorfile = f'users/{quote_plus(iri.removesuffix("#main-key"))}'
if not isfile(actorfile):
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)
with open(actorfile) as 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):
def do_POST(self):
body = self.rfile.read(int(self.headers['Content-Length']))
@ -72,10 +89,10 @@ class fuwuqi(SimpleHTTPRequestHandler):
# Make sure activity doer matches HTTP signature
actor = keyid.removesuffix('#main-key')
if 'actor' in activity and activity['actor'] != actor:
self.send_response(401)
return
if 'attributedTo' in activity and activity['attributedTo'] != actor:
if ('actor' in activity and activity['actor'] != actor) or
('attributedTo' in activity and activity['attributedTo'] != actor) or
('actor' in activity['object'] and activity['object']['actor'] != actor) or
('attributedTo' in activity['object'] and activity['object']['attributedTo'] != actor)
self.send_response(401)
return
@ -85,25 +102,36 @@ class fuwuqi(SimpleHTTPRequestHandler):
elif self.path.endswith('outbox'):
# C2S
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':
id = activity['id'].split('/')[-1]
# Post
id = activity['object']['id'].split('/')[-1]
with open(f'users/{username}.statuses/{id}', 'w') as f:
dump(activity['object'], f)
# 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'] == 'Accept':
# Accept follow request
collection_append(f'users/{username}.followers', activity['object']['actor'])
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)
# Follow request
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.end_headers()

View file

@ -7,10 +7,12 @@
"type": "Person",
"preferredUsername": "test",
"name": "Billiam Wender",
"summary": "idk",
"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",
"liked": "https://0.exozy.me/users/test.liked",
"icon": {
"type": "Image",
"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": []}