diff --git a/.gitignore b/.gitignore index 13faf01..5eb85e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +venv/ *.pem __pycache__ diff --git a/README.md b/README.md index dca9707..3547162 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,50 @@ # AP test in Flask -This is a testing area! :) +A basic ActivityPub server written in Flask, that listens to follow requests, based on this tutorial: + +This is a testing area :), to see if it is possible to use Python and Flask to write a small server that speaks ActivityPub. + +This repository was sparked from a curiosity in federation and light-weight (static) website making. + +Operating on the level of the ActivityPub protocol will hopefully give some insights in the way that networks on the Fediverse federate with each other. + +* What are the minimal requirements to run an Activity Pub server? +* How could a static site federate its content with the Fediverse? +* How are ActivityPub feeds similar and different from RSS feeds? +* How can such server be used? What kind of publishing tools can be imagined? +* What ActivityPub "objects" (is that the right term?) can we try out and use, next to the commonly used "Note" object? + +# Install this prototype + +When you run this prototype on a server, you could connect to the Fediverse. + +## Prepare your server + +For this you need to following: + +* a subdomain for the server, for example: `ap.example.com` +* a SSL certificate for this domain, for which you could use: `certbot` from +* a reverse proxy configuration, relaying the subdomain to the Flask application running on port `5010` (by default) + +Once this is set up, you can install this prototype. + +## Install the prototype + +Make a virtual environment: + +`$ python3 -m venv FOLDERNAME` + +Activate the environment: + +`$ source FOLDERNAME/bin/activate` + +Install the dependencies: + +`$ pip install -r requirements.txt` + +Run the Flask application: + +`$ python3 run.py` + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..39ea190 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +certifi==2020.11.8 +chardet==3.0.4 +click==7.1.2 +Flask==1.1.2 +idna==2.10 +itsdangerous==1.1.0 +Jinja2==2.11.2 +MarkupSafe==1.1.1 +pkg-resources==0.0.0 +pycryptodome==3.9.9 +requests==2.25.0 +urllib3==1.26.2 +Werkzeug==1.0.1 + diff --git a/tada.py b/run.py similarity index 90% rename from tada.py rename to run.py index dd2e339..c08dbc7 100644 --- a/tada.py +++ b/run.py @@ -17,6 +17,7 @@ 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') @@ -26,6 +27,7 @@ def publicKey(): 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. @@ -34,8 +36,8 @@ APP = flask.Flask(__name__) @APP.route('/', methods=['GET']) def index(): """ Displays the index page accessible at '/' """ - - content = 'test - index ({})'.format(DOMAIN) + + content = f'This ActivityPub Server is running !!! (exciting): ({ DOMAIN })' return content @APP.route('/.well-known/webfinger', methods=['GET']) @@ -55,7 +57,6 @@ def webfinger(): 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' @@ -68,13 +69,12 @@ def return_actor(actor): 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') + resp = flask.Response(json, status=200, mimetype='application/json') return resp @APP.route('/inspect') -def inspect(): +def inspect(): return flask.Response(b'

'.join(INBOX), status=200) @APP.route('/users//inbox', methods=['GET', 'POST']) @@ -86,14 +86,14 @@ def inbox(actor): $ 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 {} request.
- It came with the following header:

{}

- You have searched for the actor {}.
- This is {}'s shared inbox:

{}'''.format(flask.request.method, flask.request.headers, actor, DOMAIN, str(INBOX)) + if flask.request.method == 'GET': + return f'''This has been a { flask.request.method } request.
+ It came with the following header:

{ flask.request.headers }

+ You have searched for the actor { actor }.
+ This is { DOMAIN }'s shared inbox:

{ str(INBOX) }''' if flask.request.method == 'POST': - INBOX.append(flask.request.data) + INBOX.append(flask.request.data) return flask.Response(status=200) if __name__ == '__main__':