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
5.0 KiB
101 lines
5.0 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:5010' # 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 = 'This ActivityPub Server is running !!! (exciting): ({})'.format(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 '''This has been a <em>{}</em> request. <br>
|
|
It came with the following header: <br><br><em>{}</em><br><br>
|
|
You have searched for the actor <em>{}</em>. <br>
|
|
This is <em>{}</em>'s shared inbox: <br><br><em>{}</em>'''.format(flask.request.method, flask.request.headers, actor, DOMAIN, str(INBOX))
|
|
|
|
if flask.request.method == 'POST':
|
|
INBOX.append(flask.request.data)
|
|
return flask.Response(status=200)
|
|
|
|
if __name__ == '__main__':
|
|
APP.debug=True
|
|
APP.run(port=5010)
|