an ActivityPub testing area in Flask
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

101 lines
4.9 KiB

#!/usr/bin/env python3
import flask, os, json
# import functions
""" A basic ActivityPub server written in Flask, that listens to follow requests.
Based on this tutorial: https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/ """
"""
AP
ActivityPub
AlternativeProtocol
A...P...
"""
# Config
DOMAIN = 'https://ap.virtualprivateserver.space'
# domain = 'http://localhost:5000' # for local testing only, ActivityPub doesn't allow the usage of http:// (it only accepts https://)
def publicKey():
if not os.path.exists('./public.pem'):
os.system('openssl genrsa -out private.pem 2048')
os.system('openssl rsa -in private.pem -outform PEM -pubout -out public.pem')
else:
publicKey = open('./public.pem', 'r').read()
PUBLICKEY = publicKey.replace('\n', '\\n') # JSON-LD doesn't want to work with linebreaks,
# but needs the \n character to know where to break the line ;)
return PUBLICKEY
INBOX = []
# Create the application.
APP = flask.Flask(__name__)
@APP.route('/', methods=['GET'])
def index():
""" Displays the index page accessible at '/' """
content = f'This ActivityPub Server is running !!! (exciting): ({ DOMAIN })'
return content
@APP.route('/.well-known/webfinger', methods=['GET'])
def webfinger():
""" When you try to search for an account in eg. Mastodon,
this object is return, with a link pointing to a place
where more information about this user can be found. """
""" What is Webfinger? It is what allows us to ask a website,
“Do you have a user with this username?” and receive resource
links in response, with more information about this user. """
if flask.request.args.get('resource'):
query = flask.request.args.get('resource') # acct:alice@tada.club
actor = query.split(':')[1].split('@')[0] # alice
json = flask.render_template('webfinger.json', query=query, actor=actor, domain=DOMAIN)
resp = flask.Response(json, status=200, mimetype='application/json')
return resp
else:
return 'no query'
@APP.route('/users/<actor>', methods=['GET'])
def return_actor(actor):
""" This returns the actor.json object when somebody in the
Fediverse searches for this user. It returns the paths of
this user's inbox, its preferred username and the user's public key.
And also a profile icon (image) now! :) """
preferredUsername = actor # but could become a custom username, set by the user, stored in the database
# this preferredUsername doesn't show up yet in Mastodon ...
json = flask.render_template('actor.json', actor=actor, preferredUsername=preferredUsername, publicKey=publicKey(), domain=DOMAIN) # actor = alice
resp = flask.Response(json, status=200, mimetype='application/json')
return resp
@APP.route('/inspect')
def inspect():
return flask.Response(b'<br><br>'.join(INBOX), status=200)
@APP.route('/users/<actor>/inbox', methods=['GET', 'POST'])
def inbox(actor):
""" To post to this inbox, you could use curl:
$ curl -d '{"key" : "value"}' -H "Content-Type: application/json" -X POST http://localhost:5001/users/test/inbox
Or, with an actual Mastodon follow request:
$ curl -d '{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":{"@id":"as:movedTo","@type":"@id"},"Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"},"featured":{"@id":"toot:featured","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value"}],"id":"https://post.lurk.org/02d04ed5-dda6-48f3-a551-2e9c554de745","type":"Follow","actor":"https://post.lurk.org/users/manetta","object":"https://ap.virtualprivateserver.space/users/test","signature":{"type":"RsaSignature2017","creator":"https://post.lurk.org/users/manetta#main-key","created":"2018-11-28T16:15:35Z","signatureValue":"XUdBg+Zj9pkdOXlAYHhOtZlmU1Jdt63zwh2cXoJ8E8C1C+KvgGilkyfPTud9VNymVwdUQRl+YEW9KAZiiGaHb9H+tdVUr9BEkuR5E/tGehbMZr1sakC+qPehe4s3bRKEpJjTTJnTiSHaW7V6Qvr1u6+MVts6oj32az/ixuB/CfodSr3K/K+jZmmOl6SIUqX7Xg7xGwOxIsYaR7g9wbcJ4qyzKcTPZonPMsONq9/RSm3SeQBo7WO1FKlQiFxVP/y5eFaFP8GYDLZyK7Nj5kDL5TannfEpuF8f3oyTBErQhcFQYKcBZNbuaqX/WiIaGjtHIL2ctJe0Psb5Nfshx4MXmQ=="}}' -H "Content-Type: application/json" -X POST http://localhost:5001/users/test/inbox
"""
if flask.request.method == 'GET':
return f'''This has been a <em>{ flask.request.method }</em> request. <br>
It came with the following header: <br><br><em>{ flask.request.headers }</em><br><br>
You have searched for the actor <em>{ actor }</em>. <br>
This is <em>{ DOMAIN }</em>'s shared inbox: <br><br><em>{ str(INBOX) }</em>'''
if flask.request.method == 'POST':
INBOX.append(flask.request.data)
return flask.Response(status=200)
if __name__ == '__main__':
APP.debug=True
APP.run(port=5001)