diff --git a/activitypub/database/sqldb.py b/activitypub/database/sqldb.py index 2c9a52c..edac028 100644 --- a/activitypub/database/sqldb.py +++ b/activitypub/database/sqldb.py @@ -6,29 +6,15 @@ try: except: def create_engine(*args, **kwargs): raise Exception("You need to install sqlalchemy") - + import logging import json from ..bson import ObjectId +from ..json import JSONDecoder, JSONEncoder from .base import Database from .listdb import ListTable -class JSONEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, ObjectId): - return {"$oid": str(o)} - return super().default(o) - -class JSONDecoder(json.JSONDecoder): - def __init__(self, *args, **kwargs): - super().__init__(object_hook=self.object_hook, *args, **kwargs) - - def object_hook(self, obj): - if '$oid' not in obj: - return obj - return ObjectId(obj['$oid']) - class SQLList(): def __init__(self, database, name): self.database = database diff --git a/activitypub/json.py b/activitypub/json.py new file mode 100644 index 0000000..961d430 --- /dev/null +++ b/activitypub/json.py @@ -0,0 +1,17 @@ +import json + +class JSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ObjectId): + return {"$oid": str(o)} + return super().default(o) + +class JSONDecoder(json.JSONDecoder): + def __init__(self, *args, **kwargs): + super().__init__(object_hook=self.object_hook, *args, **kwargs) + + def object_hook(self, obj): + if '$oid' not in obj: + return obj + return ObjectId(obj['$oid']) + diff --git a/activitypub/manager/ap_routes.py b/activitypub/manager/ap_routes.py index 601c128..b3f8dd9 100644 --- a/activitypub/manager/ap_routes.py +++ b/activitypub/manager/ap_routes.py @@ -1,15 +1,19 @@ +import re + from .base import app +WEBFINGER = re.compile(r'(?:acct:)?(?P[\w.!#$%&\'*+-/=?^_`{|}~]+)@(?P[\w.:-]+)') + @app.route("/user/", ["GET"]) def route_user(self, nickname): - #obj = self.database.actors.find(id=nickname) + #obj = self.database.actors.find_one(id=nickname) obj = self.Actor(id=nickname) if obj: return self.render_json( obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//publickey", ["GET"]) def route_publickey(self, nickname): @@ -19,7 +23,7 @@ def route_publickey(self, nickname): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//outbox", ["GET"]) def route_user_outbox(self, nickname): @@ -29,7 +33,7 @@ def route_user_outbox(self, nickname): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//outbox/", ["GET"]) def route_outbox_page(self, nickname, page): @@ -39,7 +43,7 @@ def route_outbox_page(self, nickname, page): obj.to_dict() ) else: - self.error(404) + return self.error(404) #@app.route("/user//outbox", ["POST"]) #def route_(self): @@ -53,7 +57,7 @@ def route_inbox(self, nickname): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//inbox/", ["GET"]) def route_inbox_page(self, nickname, page): @@ -63,7 +67,7 @@ def route_inbox_page(self, nickname, page): obj.to_dict() ) else: - self.error(404) + return self.error(404) #@app.route("/user//inbox", ["POST"]) #def route_(self): @@ -77,7 +81,7 @@ def route_followers(self, nickname): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//followers/", ["GET"]) def route_followers_page(self, nickname, page): @@ -87,7 +91,7 @@ def route_followers_page(self, nickname, page): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//following", ["GET"]) def route_following(self, nickname): @@ -97,7 +101,7 @@ def route_following(self, nickname): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//following/", ["GET"]) def route_following_page(self, nickname, page): @@ -107,7 +111,7 @@ def route_following_page(self, nickname, page): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//liked", ["GET"]) def route_liked(self, nickname): @@ -117,7 +121,7 @@ def route_liked(self, nickname): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/user//liked/", ["GET"]) def route_liked_page(self, nickname, page): @@ -127,7 +131,7 @@ def route_liked_page(self, nickname, page): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/activity/", ["GET"]) def route_activity(self, uuid): @@ -137,7 +141,7 @@ def route_activity(self, uuid): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/activity//replies", ["GET"]) def route_activity_replies(self, uuid): @@ -147,7 +151,7 @@ def route_activity_replies(self, uuid): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/activity//replies/", ["GET"]) def route_activity_replies_page(self, uuid, page): @@ -157,7 +161,7 @@ def route_activity_replies_page(self, uuid, page): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/activity//likes", ["GET"]) def route_activity_likes(self, uuid): @@ -167,7 +171,7 @@ def route_activity_likes(self, uuid): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/activity//likes/", ["GET"]) def route_activity_likes_page(self, uuid, page): @@ -177,7 +181,7 @@ def route_activity_likes_page(self, uuid, page): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/activity//shares", ["GET"]) def route_activity_shares(self, uuid): @@ -187,7 +191,7 @@ def route_activity_shares(self, uuid): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/activity//shares/", ["GET"]) def route_activity_shares_page(self, uuid, page): @@ -197,7 +201,7 @@ def route_activity_shares_page(self, uuid, page): obj.to_dict() ) else: - self.error(404) + return self.error(404) @app.route("/content/", ["GET"]) def route_content(self, uuid): @@ -250,3 +254,117 @@ def route_content_shares_page(self, uuid, page): uuid=uuid, page=page, ) + +@app.route("/.well-known/webfinger", ["GET"]) +def route_webfinger(self): + """ + ?resource=acct:bob@my-example.com + """ + resource = self.get_argument("resource") + obj = None + if resource: + matches = WEBFINGER.match(resource) + if matches: + username = matches["username"] + host = matches["host"] + id = "https://%s/%s" % (host, username) + obj = self.database.actors.find_one({"id":id}) + ## Add a temp person: + #obj = self.Person(id="dsblank") + #self.database.actors.insert_one(obj.to_dict()) + if obj: + return self.render_json({ + "subject": resource, + "links": [{ + "rel": "self", + "type": "application/activity+json", + "href": obj["id"] + }] + }) + else: + return self.error(404) + +""" + +{ + "subject": "acct:will@willnorris.com", + "aliases": [ + "mailto:will@willnorris.com", + "https://willnorris.com/" + ], + "links": [ + { + "rel": "http://webfinger.net/rel/avatar", + "href": "https://willnorris.com/logo.jpg", + "type": "image/jpeg" + }, + { + "rel": "http://webfinger.net/rel/profile-page", + "href": "https://willnorris.com/", + "type": "text/html" + } + ] +} + +{ + "subject": "acct:alice@my-example.com", + "links": [ + { + "rel": "self", + "type": "application/activity+json", + "href": "https://my-example.com/actor" + } + ] +} +""" + +# /api/v1/instance + +""" +{ + "uri":"mastodon.social", + "title":"Mastodon", + "description":"This page describes the mastodon.social \u003cem\u003einstance\u003c/em\u003e - wondering what Mastodon is? Check out \u003ca href=\"https://joinmastodon.org\"\u003ejoinmastodon.org\u003c/a\u003e instead! In essence, Mastodon is a decentralized, open source social network. This is just one part of the network, run by the main developers of the project \u003cimg draggable=\"false\" alt=\"🐘\" class=\"emojione\" src=\"https://mastodon.social/emoji/1f418.svg\" /\u003e It is not focused on any particular niche interest - everyone is welcome as long as you follow our code of conduct!", + "email":"hello@joinmastodon.org", + "version":"2.4.3", + "urls": { + "streaming_api":"wss://mastodon.social" + }, + "stats": { + "user_count":171746, + "status_count":6467133, + "domain_count":5177 + }, + "thumbnail":"https://files.mastodon.social/site_uploads/files/000/000/001/original/DN5wMUeVQAENPwp.jpg_large.jpeg", + "languages":["en"], + "contact_account": { + "id":"1", + "username":"Gargron", + "acct":"Gargron", + "display_name":"Eugen", + "locked":false, + "bot":false, + "created_at":"2016-03-16T14:34:26.392Z", + "note":"\u003cp\u003eDeveloper of Mastodon. 25\u003c/p\u003e", + "url":"https://mastodon.social/@Gargron", + "avatar":"https://files.mastodon.social/accounts/avatars/000/000/001/original/eb9e00274b135808.png", + "avatar_static":"https://files.mastodon.social/accounts/avatars/000/000/001/original/eb9e00274b135808.png", + "header":"https://files.mastodon.social/accounts/headers/000/000/001/original/af58e4df0e8b3e15.png", + "header_static":"https://files.mastodon.social/accounts/headers/000/000/001/original/af58e4df0e8b3e15.png", + "followers_count":92732, + "following_count":530, + "statuses_count":40388, + "emojis":[], + "fields":[ + { + "name":"Patreon", + "value":"\u003ca href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003epatreon.com/mastodon\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" + }, + { + "name":"Liberapay", + "value":"\u003ca href=\"https://liberapay.com/Mastodon/\" rel=\"me nofollow noopener\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003eliberapay.com/Mastodon/\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" + } + ] + } +} +""" diff --git a/activitypub/manager/base.py b/activitypub/manager/base.py index 406bd0c..e939d1f 100644 --- a/activitypub/manager/base.py +++ b/activitypub/manager/base.py @@ -15,6 +15,7 @@ class DataWrapper(): """ Instance for saving routes, filters, etc. for app. + >>> app.clear() >>> @app.filter ... def upper(item): ... return item.upper() @@ -37,6 +38,9 @@ class DataWrapper(): """ _data = Data() + def clear(self): + self._data.clear() + def filter(self, f): """ Wrap a plain function/method to provide template function. @@ -131,7 +135,7 @@ class Manager(): pass def error(self, error_number): - self.render_template("%s.html" % error_number) + return self.render_template("%s.html" % error_number) @property def request(self): diff --git a/activitypub/manager/flaskman.py b/activitypub/manager/flaskman.py index acd15dd..960f6d2 100644 --- a/activitypub/manager/flaskman.py +++ b/activitypub/manager/flaskman.py @@ -17,6 +17,9 @@ class FlaskManager(Manager): def render_json(self, obj): return jsonify(obj) # has correct header type set + def get_argument(self, name, default_value=None): + return request.args.get(name, default_value) + def render_template(self, template_name, **kwargs): ## TODO : add context_processor q = { diff --git a/activitypub/manager/tornadoman.py b/activitypub/manager/tornadoman.py index 6a63347..7093635 100644 --- a/activitypub/manager/tornadoman.py +++ b/activitypub/manager/tornadoman.py @@ -19,7 +19,9 @@ def make_handler(f, manager, methods): class Handler(RequestHandler): def get(self, *args, **kwargs): self.database = manager.database + ## TODO: add these in a more dynamic manner? self.Actor = manager.Actor + self.Person = manager.Person self.Activity = manager.Activity self.Note = manager.Note return f(self, *args, **kwargs)