From 2c6cb1e48d8927de1229f57573a8b938d7cd5434 Mon Sep 17 00:00:00 2001 From: Douglas Blank Date: Mon, 23 Jul 2018 11:48:40 -0400 Subject: [PATCH] Allow connection to any Redis db; rerenamed Dummy to List; refactor tables; fixed bug in Redis edit/delete items --- activitypub/bson/objectid.py | 4 +- activitypub/database/__init__.py | 2 +- activitypub/database/base.py | 26 +- activitypub/database/{dummy.py => listdb.py} | 266 ++++++++++--------- activitypub/database/mongodb.py | 3 +- activitypub/database/redisdb.py | 18 +- activitypub/manager/base.py | 18 +- activitypub/manager/flaskman.py | 23 +- activitypub/manager/tornadoman.py | 37 ++- apps/tornado/app.py | 10 - apps/tornado/app/__main__.py | 3 - apps/tornado/app/app.py | 84 ------ apps/tornado/app/backend.py | 179 ------------- apps/tornado/app/handlers.py | 134 ---------- apps/tornado/app/key.py | 41 --- apps/tornado/app/webfinger.py | 77 ------ apps/tornado/static/chat.css | 62 ----- apps/tornado/static/chat.js | 154 ----------- apps/tornado/templates/index.html | 47 ---- apps/tornado/templates/login.html | 39 --- apps/tornado/templates/message.html | 9 - 21 files changed, 248 insertions(+), 988 deletions(-) rename activitypub/database/{dummy.py => listdb.py} (61%) delete mode 100644 apps/tornado/app.py delete mode 100644 apps/tornado/app/__main__.py delete mode 100644 apps/tornado/app/app.py delete mode 100644 apps/tornado/app/backend.py delete mode 100644 apps/tornado/app/handlers.py delete mode 100644 apps/tornado/app/key.py delete mode 100644 apps/tornado/app/webfinger.py delete mode 100644 apps/tornado/static/chat.css delete mode 100644 apps/tornado/static/chat.js delete mode 100644 apps/tornado/templates/index.html delete mode 100644 apps/tornado/templates/login.html delete mode 100644 apps/tornado/templates/message.html diff --git a/activitypub/bson/objectid.py b/activitypub/bson/objectid.py index 4d63c01..5035f4f 100644 --- a/activitypub/bson/objectid.py +++ b/activitypub/bson/objectid.py @@ -148,8 +148,8 @@ class ObjectId(object): >>> gen_time = datetime.datetime(2010, 1, 1) >>> early = ObjectId.from_datetime(gen_time) >>> from activitypub.manager import Manager - >>> from activitypub.database import DummyDatabase - >>> m = Manager(database=DummyDatabase()) + >>> from activitypub.database import ListDatabase + >>> m = Manager(database=ListDatabase()) >>> n = m.Note(_id=early, attributedTo="alyssa") >>> m.database.activities.insert_one(n.to_dict()) >>> gen_time = datetime.datetime(2011, 1, 1) diff --git a/activitypub/database/__init__.py b/activitypub/database/__init__.py index 6f0a8ba..1465cd1 100644 --- a/activitypub/database/__init__.py +++ b/activitypub/database/__init__.py @@ -1,5 +1,5 @@ from .base import Database, Table -from .dummy import DummyDatabase +from .listdb import ListDatabase from .mongodb import MongoDatabase from .redisdb import RedisDatabase diff --git a/activitypub/database/base.py b/activitypub/database/base.py index a83e500..9b6e89e 100644 --- a/activitypub/database/base.py +++ b/activitypub/database/base.py @@ -1,16 +1,3 @@ -class Database(): - """ - Base database class. - """ - def __init__(self, Table): - self.activities = Table(self, "activities") - self.u2f = Table(self, "u2f") - self.instances = Table(self, "instances") - self.actors = Table(self, "actors") - self.indieauth = Table(self, "indieauth") - - ## TODO: put required methods to override here - class Table(): """ Base table class. @@ -19,4 +6,15 @@ class Table(): self.database = database self.name = name - ## TODO: put required methods to override here +class Database(): + """ + Base database class. + """ + Table = Table + def __init__(self): + self._tables = {} + + def __getattr__(self, attr): + if attr not in self._tables: + self._tables[attr] = self.Table(self, attr) + return self._tables[attr] diff --git a/activitypub/database/dummy.py b/activitypub/database/listdb.py similarity index 61% rename from activitypub/database/dummy.py rename to activitypub/database/listdb.py index 0970a7f..f6aea3c 100644 --- a/activitypub/database/dummy.py +++ b/activitypub/database/listdb.py @@ -6,46 +6,6 @@ import copy from ..bson import ObjectId from .base import Database, Table -def get_item_in_dict(dictionary, item): - """ - Get dictionary item from a dotted-word. - - >>> get_item_in_dict({"x": 1}, "x") - 1 - >>> get_item_in_dict({"x": {"y": 42}}, "x.y") - 42 - """ - current = dictionary - for word in item.split("."): - if word in current: - current = current[word] - else: - return None - return current - -def set_item_in_dict(dictionary, item, value): - """ - Set dictionary item from a dotted-word. - - >>> d = {"x": 1} - >>> get_item_in_dict(d, "x") - 1 - >>> set_item_in_dict(d, "x", 2) - >>> get_item_in_dict(d, "x") - 2 - >>> d2 = {"x": {"y": 42}} - >>> get_item_in_dict(d2, "x.y") - 42 - >>> set_item_in_dict(d2, "x.y", 43) - >>> get_item_in_dict(d2, "x.y") - 43 - """ - current = dictionary - words = item.split(".") - for word in words[:-1]: - current = current[word] - current[words[-1]] = value - def is_match(lhs, rhs): """ >>> is_match(12, 12) @@ -91,55 +51,7 @@ def is_match(lhs, rhs): else: return lhs == rhs -def match(doc, query): - """ - Does a dictionary match a (possibly-nested) query/dict? - - query is a dictionary of dotted words or query operations. - - >>> match({"x": 42}, {"x": 42}) - True - >>> match({"x": 42}, {"x": 43}) - False - >>> match({"x": {"y": 5}}, {"x.y": 5}) - True - >>> match({"x": {"y": 5}}, {"x.y": 4}) - False - >>> activity = {"name": "Joe"} - >>> match(activity, {"$and": [{"name": "Sally"}, {"name": "Joe"}]}) - False - >>> match(activity, {"$and": [{"name": "Joe"}, {"name": "Sally"}]}) - False - >>> match(activity, {"$or": [{"name": "Sally"}, {"name": "Joe"}]}) - True - >>> match(activity, {"$or": [{"name": "Joe"}, {"name": "Sally"}]}) - True - """ - for item in query: - if item == "$or": - matched = False - for each in query[item]: - if match(doc, each): - matched = True - break - if not matched: - return False - elif item == "$and": - matched = True - for each in query[item]: - if not match(doc, each): - matched = False - break - if not matched: - return False - else: - thing = get_item_in_dict(doc, item) - matched = is_match(thing, query[item]) - if not matched: - return False - return True - -class DummyTable(Table): +class ListTable(Table): def __init__(self, database=None, name=None, data=None): super().__init__(database, name) self.data = data or [] @@ -147,6 +59,12 @@ class DummyTable(Table): def __getitem__(self, item): return self.data[item] + def __setitem__(self, item, value): + self.data[item] = value + + def __delitem__(self, key): + del self.data[key] + def __len__(self): return len(self.data) @@ -156,20 +74,117 @@ class DummyTable(Table): def __repr__(self): return repr(self.data) + def match(self, doc, query): + """ + Does a dictionary match a (possibly-nested) query/dict? + + query is a dictionary of dotted words or query operations. + + >>> table = ListTable() + >>> table.match({"x": 42}, {"x": 42}) + True + >>> table.match({"x": 42}, {"x": 43}) + False + >>> table.match({"x": {"y": 5}}, {"x.y": 5}) + True + >>> table.match({"x": {"y": 5}}, {"x.y": 4}) + False + >>> activity = {"name": "Joe"} + >>> table.match(activity, {"$and": [{"name": "Sally"}, {"name": "Joe"}]}) + False + >>> table.match(activity, {"$and": [{"name": "Joe"}, {"name": "Sally"}]}) + False + >>> table.match(activity, {"$or": [{"name": "Sally"}, {"name": "Joe"}]}) + True + >>> table.match(activity, {"$or": [{"name": "Joe"}, {"name": "Sally"}]}) + True + """ + for item in query: + if item == "$or": + matched = False + for each in query[item]: + if self.match(doc, each): + matched = True + break + if not matched: + return False + elif item == "$and": + matched = True + for each in query[item]: + if not self.match(doc, each): + matched = False + break + if not matched: + return False + else: + thing = self.get_item_in_dict(doc, item) + matched = is_match(thing, query[item]) + if not matched: + return False + return True + + def get_item_in_dict(self, dictionary, item): + """ + Get dictionary item from a dotted-word. + + >>> table = ListTable() + >>> table.get_item_in_dict({"x": 1}, "x") + 1 + >>> table.get_item_in_dict({"x": {"y": 42}}, "x.y") + 42 + """ + current = dictionary + for word in item.split("."): + if word in current: + current = current[word] + else: + return None + return current + + def set_item_in_dict(self, dictionary, item, value, i=None): + """ + Set dictionary item from a dotted-word. + + >>> table = ListTable() + >>> d = {"x": 1} + >>> table.get_item_in_dict(d, "x") + 1 + >>> table.set_item_in_dict(d, "x", 2) + >>> table.get_item_in_dict(d, "x") + 2 + >>> d2 = {"x": {"y": 42}} + >>> table.get_item_in_dict(d2, "x.y") + 42 + >>> table.set_item_in_dict(d2, "x.y", 43) + >>> table.get_item_in_dict(d2, "x.y") + 43 + """ + current = dictionary + words = item.split(".") + for word in words[:-1]: + current = current[word] + current[words[-1]] = value + ## update the database for those lists + ## that are separate from the reference + ## in dictionary (eg, redis) + if i is not None: + self.data[i] = dictionary + def drop(self): self.data.clear() def sort(self, sort_key, sort_order): # sort_key = "_id" # sort_order = 1 or -1 - return DummyTable(data=sorted( + ## Always use ListTable here: + return ListTable(data=sorted( self.data, - key=lambda row: get_item_in_dict(row, sort_key), + key=lambda row: self.get_item_in_dict(row, sort_key), reverse=(sort_order == -1))) def insert_one(self, row): """ - >>> table = DummyTable() + >>> table = ListTable() >>> table.count() 0 >>> len(table.data) @@ -199,9 +214,9 @@ class DummyTable(Table): row["_id"] = ObjectId() self.data.append(row) - def find(self, query=None, limit=None): + def find(self, query=None, limit=None, enumerated=False): """ - >>> table = DummyTable() + >>> table = ListTable() >>> table.insert_one({"a": 1, "b": 2}) >>> table.find({"a": 1}) # doctest: +ELLIPSIS [{'a': 1, 'b': 2, '_id': ObjectId('...')}] @@ -210,17 +225,29 @@ class DummyTable(Table): """ if query is not None: if limit is not None: - return DummyTable(data=[doc for doc in self.data if match(doc, query)][:limit]) + if enumerated: + return [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)][:limit] + else: + return ListTable(data=[doc for doc in self.data if self.match(doc, query)][:limit]) else: - return DummyTable(data=[doc for doc in self.data if match(doc, query)]) + if enumerated: + return [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)] + else: + return ListTable(data=[doc for doc in self.data if self.match(doc, query)]) elif limit is not None: - return DummyTable(data=self.data[:limit]) + if enumerated: + return list(enumerated(self.data[:limit])) + else: + return ListTable(data=self.data[:limit]) else: - return self + if enumerated: + return list(enumerated(self.data)) + else: + return self def remove(self, query=None): """ - >>> table = DummyTable() + >>> table = ListTable() >>> table.insert_one({"a": 1, "b": 2}) >>> table.insert_one({"c": 3, "d": 4}) >>> table.find({"a": 1}) # doctest: +ELLIPSIS @@ -231,13 +258,13 @@ class DummyTable(Table): [{'c': 3, 'd': 4, '_id': ObjectId('...')}] """ if query: - items = [doc for doc in self.data if match(doc, query)] + items = [doc for doc in self.data if self.match(doc, query)] # delete them else: self.data = [] def find_one(self, query): - results = [doc for doc in self.data if match(doc, query)] + results = [doc for doc in self.data if self.match(doc, query)] if results: return results[0] else: @@ -252,12 +279,12 @@ class DummyTable(Table): return True def update_one(self, query, items, upsert=False): - results = [doc for doc in self.data if match(doc, query)] + results = [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)] if len(results) > 0: self.process_updates(results[:1], items) elif upsert: ## update and insert self.insert_one(query) - results = self.find(query) + results = self.find(query, enumerated=True) self.process_updates(results, items) def process_updates(self, results, items): @@ -269,49 +296,49 @@ class DummyTable(Table): * {'$set': {'meta.undo': True, 'meta.exta': 'object deleted'}} * {'$inc': {'meta.count_reply': -1, 'meta.count_direct_reply': -1}} - >>> db = DummyDatabase() + >>> db = ListDatabase() >>> db.actors.insert_one({'meta': {'deleted': True}}) >>> len(db.actors.find({'meta.deleted': True})) 1 - >>> db.actors.process_updates(db.actors.data, + >>> db.actors.process_updates(enumerate(db.actors.data), ... {"$set": {'meta.deleted': False}}) >>> len(db.actors.find({'meta.deleted': True})) 0 >>> len(db.actors.find({'meta.deleted': False})) 1 - >>> db = DummyDatabase() + >>> db = ListDatabase() >>> db.actors.insert_one({'meta': {'count': 0}}) >>> db.actors.find()[0]["meta"]["count"] 0 - >>> db.actors.process_updates(db.actors.data, + >>> db.actors.process_updates(enumerate(db.actors.data), ... {"$inc": {'meta.count': +1}}) >>> db.actors.find()[0]["meta"]["count"] 1 - >>> db.actors.process_updates(db.actors.data, + >>> db.actors.process_updates(enumerate(db.actors.data), ... {"$inc": {'meta.count': +1}}) >>> db.actors.find()[0]["meta"]["count"] 2 - >>> db.actors.process_updates(db.actors.data, + >>> db.actors.process_updates(enumerate(db.actors.data), ... {"$inc": {'meta.count': -1}}) >>> db.actors.find()[0]["meta"]["count"] 1 """ - for result in results: + for i,result in results: for item in items: # key if item == "$set": for thing in items[item]: # keys of the $set value = items[item][thing] - set_item_in_dict(result, thing, value) + self.set_item_in_dict(result, thing, value, i) elif item == "$inc": for thing in items[item]: - old_value = get_item_in_dict(result, thing) + old_value = self.get_item_in_dict(result, thing) incr = items[item][thing] - set_item_in_dict(result, thing, old_value + incr) + self.set_item_in_dict(result, thing, old_value + incr, i) def update(self, query, items, upsert=False): """ - >>> db = DummyDatabase() + >>> db = ListDatabase() >>> q = {"id": "XXX", "test": 42} >>> db.actors.update(q, {"$inc": {"test": +1}}, upsert=False) >>> len(db.actors.find(q)) @@ -323,25 +350,24 @@ class DummyTable(Table): >>> len(db.actors.find(q)) 1 """ - results = [doc for doc in self.data if match(doc, query)] + results = [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)] if len(results) > 0: self.process_updates(results, items) elif upsert: # update and insert self.insert_one(query) - results = self.find(query) + results = self.find(query, enumerated=True) self.process_updates(results, items) def count(self, query=None): if query: - return len([doc for doc in self.data if match(doc, query)]) + return len([doc for doc in self.data if self.match(doc, query)]) else: return len(self.data) count_documents = count -class DummyDatabase(Database): - def __init__(self): - super().__init__(DummyTable) +class ListDatabase(Database): + Table = ListTable """ process_updates: [{'box': 'outbox', 'activity': {'type': 'Create', 'actor': 'http://localhost:5005', 'object': {'type': 'Note', 'sensitive': False, 'attributedTo': 'http://localhost:5005', 'cc': ['http://localhost:5005/followers'], 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'content': '

2

', 'tag': [], 'published': '2018-07-19T17:23:22Z', 'id': 'http://localhost:5005/outbox/75b40a6f5c319bdf/activity', 'url': 'http://localhost:5005/note/75b40a6f5c319bdf'}, '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', {'Hashtag': 'as:Hashtag', 'sensitive': 'as:sensitive'}], 'published': '2018-07-19T17:23:22Z', 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'cc': ['http://localhost:5005/followers'], 'id': 'http://localhost:5005/outbox/75b40a6f5c319bdf'}, 'type': ['Create'], 'remote_id': 'http://localhost:5005/outbox/75b40a6f5c319bdf', 'meta': {'undo': False, 'deleted': False}, '_id': ObjectId('5b50c90a1342a3318e13d434'), '_requested': True}] diff --git a/activitypub/database/mongodb.py b/activitypub/database/mongodb.py index d468154..d1780c5 100644 --- a/activitypub/database/mongodb.py +++ b/activitypub/database/mongodb.py @@ -42,9 +42,10 @@ class MongoTable(Table): self.__dict__[attr] = value class MongoDatabase(Database): + Table = MongoTable def __init__(self, uri, db_name): + super().__init__() self.uri = uri self.client = MongoClient(self.uri) self.db_name = db_name self.DB = self.client[self.db_name] - super().__init__(MongoTable) diff --git a/activitypub/database/redisdb.py b/activitypub/database/redisdb.py index 0609f32..109f16b 100644 --- a/activitypub/database/redisdb.py +++ b/activitypub/database/redisdb.py @@ -1,12 +1,20 @@ from redis_collections import List -from .dummy import DummyTable +import redis +from .listdb import ListTable from .base import Database -class RedisTable(DummyTable): +class RedisTable(ListTable): def __init__(self, database=None, name=None, data=None): super().__init__(database, name) - self.data = List(key=name, data=data) + self.data = List(key=name, data=data, redis=self.database.redis) class RedisDatabase(Database): - def __init__(self): - super().__init__(RedisTable) + Table = RedisTable + def __init__(self, url=None, **kwargs): + """ + url + * "redis://localhost:6379" + * "redis://localhost:6379/0" + """ + self.redis = redis.StrictRedis.from_url(url, **kwargs) if url else None + super().__init__() diff --git a/activitypub/manager/base.py b/activitypub/manager/base.py index c09c4ef..e5bf2c0 100644 --- a/activitypub/manager/base.py +++ b/activitypub/manager/base.py @@ -2,20 +2,33 @@ import binascii import os import uuid +class Routes(): + routes = [] + + def __call__(self, path, methods=["GET"]): + print("call Routes!") + def decorator(function): + print("call decorator!") + Routes.routes.append((path, function)) + print("return") + return decorator + class Manager(): """ Manager class that ties together ActivityPub objects, defaults, and a database. >>> from activitypub.manager import Manager - >>> from activitypub.database import DummyDatabase - >>> db = DummyDatabase() + >>> from activitypub.database import ListDatabase + >>> db = ListDatabase() >>> manager = Manager(database=db) >>> """ app_name = "activitypub" version = "1.0.0" key_path = "./keys" + route = Routes() + def __init__(self, context=None, defaults=None, database=None): from ..classes import ActivityPubBase self.callback = lambda box, activity_id: None @@ -236,4 +249,3 @@ class Manager(): def decorator(function): return function return decorator - diff --git a/activitypub/manager/flaskman.py b/activitypub/manager/flaskman.py index fcd60ce..bc9e69e 100644 --- a/activitypub/manager/flaskman.py +++ b/activitypub/manager/flaskman.py @@ -2,17 +2,36 @@ from flask import (Flask, Response, abort, jsonify as flask_jsonify, redirect, render_template, request, session, url_for) - from flask_wtf.csrf import CSRFProtect from .base import Manager +class FlaskRoutes(): + def __init__(self, manager): + self.manager = manager + + def __call__(self, path, methods=["GET"]): + print("Calling FlaskRoutes() with path=", path) + def decorator(function): + print("wrapping!") + @self.manager.app.route(path) + def f(*args, **kwargs): + print("calling wrapped function!") + function(*args, **kwargs) + print("returning") + return decorator + class FlaskManager(Manager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.app = Flask(__name__) self.app.config.update(WTF_CSRF_CHECK_DEFAULT=False) - self.csrf = CSRFProtect(app) + self.csrf = CSRFProtect(self.app) + print("here!") + self.route = FlaskRoutes(self) + + def run(self): + self.app.run(debug=1) def load_secret_key(self, name): key = self._load_secret_key(name) diff --git a/activitypub/manager/tornadoman.py b/activitypub/manager/tornadoman.py index 8413610..49d9f9c 100644 --- a/activitypub/manager/tornadoman.py +++ b/activitypub/manager/tornadoman.py @@ -1,6 +1,41 @@ +import tornado +from tornado.web import (Application, RequestHandler) + from .base import Manager +class TornadoRoutes(): + routes = [] + def __init__(self, manager): + self.manager = manager + + def __call__(self, path, methods=["GET"]): + def decorator(function): + for method in methods: + if method == "GET": + class TempHandler(RequestHandler): + def get(self, manager, *args, **kwargs): + function(manager, *args, **kwargs) + elif method == "POST": + class TempHandler(RequestHandler): + def post(self, manager, *args, **kwargs): + function(manager, *args, **kwargs) + TornadoRoutes.routes.append((path, TempHandler)) + return decorator + class TornadoManager(Manager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - #self.app = Flask(__name__) + self.route = TornadoRoutes() + + def login_required(): + tornado.web.authenticated + + def run(self): + self.app = Application() + #super().__init__([url(handler[0], + # handler[1], + # handler[3], + # name=handler[2]) + # for handler in handlers], **settings) + self.app.listen(5005) + tornado.ioloop.IOLoop.current().start() diff --git a/apps/tornado/app.py b/apps/tornado/app.py deleted file mode 100644 index 0ad0b06..0000000 --- a/apps/tornado/app.py +++ /dev/null @@ -1,10 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from .models import Base - -class Application(): - def __init__(self, engine_string): - self.engine = create_engine(engine_string) - Base.metadata.create_all(self.engine) - DBSession = sessionmaker(bind=self.engine) - self.session = DBSession() diff --git a/apps/tornado/app/__main__.py b/apps/tornado/app/__main__.py deleted file mode 100644 index b982e5d..0000000 --- a/apps/tornado/app/__main__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .app import main - -main() diff --git a/apps/tornado/app/app.py b/apps/tornado/app/app.py deleted file mode 100644 index 88f2e57..0000000 --- a/apps/tornado/app/app.py +++ /dev/null @@ -1,84 +0,0 @@ -import logging -import os - -import tornado.web -import tornado.log -from tornado.options import define, options, parse_command_line -from passlib.hash import sha256_crypt as crypt - -from .handlers import (MessageNewHandler, MessageUpdatesHandler, - MainHandler, MessageBuffer, LoginHandler, - LogoutHandler) - -define("port", default=8888, help="run on the given port", type=int) -define("debug", default=False, help="run in debug mode", type=bool) -define("directory", default=".", help="location of templates and static", type=str) - -APPLICATION_NAME = "ActivityPub" - -### To change the URLs each handler serves, you'll need to edit: -### 1. This code -### 2. The static/chat.js code -### 3. The template/*.html code - -class ActivityPubApplication(tornado.web.Application): - """ - """ - def __init__(self, *args, **kwargs): - self.message_buffer = MessageBuffer() - super().__init__(*args, **kwargs) - - def get_user_data(self, getusername): - return { - "password": crypt.hash(getusername), - } - - def handle_messages(self, messages): - for message in messages: - tornado.log.logging.info("handle_message: %s", message) - self.message_buffer.new_messages(messages) - -def make_url(path, handler, kwargs=None, name=None): - #kwargs["options"] = options - return tornado.web.url(path, handler, kwargs, name) - -def make_app(): - parse_command_line() - if options.debug: - import tornado.autoreload - log = logging.getLogger() - log.setLevel(logging.DEBUG) - tornado.log.logging.info("Debug mode...") - template_directory = os.path.join(options.directory, 'templates') - tornado.log.logging.info(template_directory) - for dirpath, dirnames, filenames in os.walk(template_directory): - for filename in filenames: - template_filename = os.path.join(dirpath, filename) - tornado.log.logging.info(" watching: " + os.path.relpath(template_filename)) - tornado.autoreload.watch(template_filename) - app = ActivityPubApplication( - [ - make_url(r"/", MainHandler, name="home"), - make_url(r'/login', LoginHandler, name="login"), - make_url(r'/logout', LogoutHandler, name="logout"), - make_url(r"/message/new", MessageNewHandler), - make_url(r"/message/updates", MessageUpdatesHandler), - ], - cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", - login_url = "/login", - template_path=os.path.join(os.path.dirname(options.directory), "templates"), - static_path=os.path.join(os.path.dirname(options.directory), "static"), - xsrf_cookies=True, - debug=options.debug, - ) - return app - -def main(): - app = make_app() - app.listen(options.port) - tornado.log.logging.info("Starting...") - try: - tornado.ioloop.IOLoop.current().start() - except KeyboardInterrupt: - tornado.log.logging.info("Shutting down...") - tornado.log.logging.info("Stopped.") diff --git a/apps/tornado/app/backend.py b/apps/tornado/app/backend.py deleted file mode 100644 index ef94b7a..0000000 --- a/apps/tornado/app/backend.py +++ /dev/null @@ -1,179 +0,0 @@ -import abc -import binascii -import os -import typing - -import requests - -from .__version__ import __version__ -from .errors import ActivityNotFoundError -from .errors import RemoteActivityGoneError -from .urlutils import check_url as check_url - -if typing.TYPE_CHECKING: - from little_boxes import activitypub as ap # noqa: type checking - - -class Backend(abc.ABC): - def debug_mode(self) -> bool: - """Should be overidded to return `True` in order to enable the debug mode.""" - return False - - def check_url(self, url: str) -> None: - check_url(url, debug=self.debug_mode()) - - def user_agent(self) -> str: - return ( - f"{requests.utils.default_user_agent()} (Little Boxes/{__version__};" - " +http://github.com/tsileo/little-boxes)" - ) - - def random_object_id(self) -> str: - """Generates a random object ID.""" - return binascii.hexlify(os.urandom(8)).decode("utf-8") - - def fetch_json(self, url: str, **kwargs): - self.check_url(url) - resp = requests.get( - url, - headers={"User-Agent": self.user_agent(), "Accept": "application/json"}, - **kwargs, - ) - - resp.raise_for_status() - - return resp - - def is_from_outbox( - self, as_actor: "ap.Person", activity: "ap.BaseActivity" - ) -> bool: - return activity.get_actor().id == as_actor.id - - @abc.abstractmethod - def post_to_remote_inbox( - self, as_actor: "ap.Person", payload_encoded: str, recp: str - ) -> None: - pass # pragma: no cover - - @abc.abstractmethod - def base_url(self) -> str: - pass # pragma: no cover - - def fetch_iri(self, iri: str, **kwargs) -> "ap.ObjectType": # pragma: no cover - self.check_url(iri) - resp = requests.get( - iri, - headers={ - "User-Agent": self.user_agent(), - "Accept": "application/activity+json", - }, - **kwargs, - ) - if resp.status_code == 404: - raise ActivityNotFoundError(f"{iri} is not found") - elif resp.status_code == 410: - raise RemoteActivityGoneError(f"{iri} is gone") - - resp.raise_for_status() - - return resp.json() - - @abc.abstractmethod - def inbox_check_duplicate(self, as_actor: "ap.Person", iri: str) -> bool: - pass # pragma: no cover - - @abc.abstractmethod - def activity_url(self, obj_id: str) -> str: - pass # pragma: no cover - - @abc.abstractmethod - def note_url(self, obj_id: str) -> str: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_create(self, as_actor: "ap.Person", activity: "ap.Create") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_delete(self, as_actor: "ap.Person", activity: "ap.Delete") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def inbox_create(self, as_actor: "ap.Person", activity: "ap.Create") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def inbox_delete(self, as_actor: "ap.Person", activity: "ap.Delete") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_is_blocked(self, as_actor: "ap.Person", actor_id: str) -> bool: - pass # pragma: no cover - - @abc.abstractmethod - def inbox_new(self, as_actor: "ap.Person", activity: "ap.BaseActivity") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_new(self, as_actor: "ap.Person", activity: "ap.BaseActivity") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def new_follower(self, as_actor: "ap.Person", follow: "ap.Follow") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def new_following(self, as_actor: "ap.Person", follow: "ap.Follow") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def undo_new_follower(self, as_actor: "ap.Person", follow: "ap.Follow") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def undo_new_following(self, as_actor: "ap.Person", follow: "ap.Follow") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def inbox_update(self, as_actor: "ap.Person", activity: "ap.Update") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_update(self, as_actor: "ap.Person", activity: "ap.Update") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def inbox_like(self, as_actor: "ap.Person", activity: "ap.Like") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def inbox_undo_like(self, as_actor: "ap.Person", activity: "ap.Like") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_like(self, as_actor: "ap.Person", activity: "ap.Like") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_undo_like(self, as_actor: "ap.Person", activity: "ap.Like") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def inbox_announce(self, as_actor: "ap.Person", activity: "ap.Announce") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def inbox_undo_announce( - self, as_actor: "ap.Person", activity: "ap.Announce" - ) -> None: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_announce(self, as_actor: "ap.Person", activity: "ap.Announce") -> None: - pass # pragma: no cover - - @abc.abstractmethod - def outbox_undo_announce( - self, as_actor: "ap.Person", activity: "ap.Announce" - ) -> None: - pass # pragma: no cover diff --git a/apps/tornado/app/handlers.py b/apps/tornado/app/handlers.py deleted file mode 100644 index a512b5e..0000000 --- a/apps/tornado/app/handlers.py +++ /dev/null @@ -1,134 +0,0 @@ -import uuid - -import tornado.escape -import tornado.web -import tornado.log -from tornado.concurrent import Future -from tornado import gen -from passlib.hash import sha256_crypt as crypt - -class BaseHandler(tornado.web.RequestHandler): - def prepare(self): - super().prepare() - self.json_data = None - if self.request.headers.get("Content-Type") == "application/json": - if self.request.body: - try: - self.json_data = tornado.escape.json_decode(self.request.body) - except ValueError: - tornado.log.logging.info("unable to decode JSON data (%s)", self.request.body) - - def get_current_user(self): - user = self.get_secure_cookie("user") - if isinstance(user, bytes): - user = user.decode() - return user - -class LoginHandler(BaseHandler): - def get(self): - self.render('login.html') - - def post(self): - getusername = self.get_argument("username") - getpassword = self.get_argument("password") - user_data = self.application.get_user_data(getusername) - tornado.log.logging.info("user_data[password]=%s", user_data["password"]) - tornado.log.logging.info("getpassword=%s", getpassword) - if user_data and user_data["password"] and crypt.verify(getpassword, user_data["password"]): - self.set_secure_cookie("user", self.get_argument("username")) - self.redirect(self.get_argument("next", self.reverse_url("home"))) - else: - self.redirect(self.reverse_url("login")) - -class LogoutHandler(BaseHandler): - def get(self): - self.clear_cookie("user") - self.redirect(self.get_argument("next", self.reverse_url("home"))) - -class MessageBuffer(object): - def __init__(self): - self.waiters = set() - self.cache = [] - self.cache_size = 200 - - def wait_for_messages(self, cursor=None): - # Construct a Future to return to our caller. This allows - # wait_for_messages to be yielded from a coroutine even though - # it is not a coroutine itself. We will set the result of the - # Future when results are available. - result_future = Future() - if cursor: - new_count = 0 - for msg in reversed(self.cache): - if msg["id"] == cursor: - break - new_count += 1 - if new_count: - result_future.set_result(self.cache[-new_count:]) - return result_future - self.waiters.add(result_future) - return result_future - - def cancel_wait(self, future): - self.waiters.remove(future) - # Set an empty result to unblock any coroutines waiting. - future.set_result([]) - - def new_messages(self, messages): - tornado.log.logging.info("Sending new message to %r listeners", len(self.waiters)) - for future in self.waiters: - future.set_result(messages) - self.waiters = set() - self.cache.extend(messages) - if len(self.cache) > self.cache_size: - self.cache = self.cache[-self.cache_size:] - -class MainHandler(BaseHandler): - @tornado.web.authenticated - def get(self): - user = self.get_current_user() - #messages = [message for message in self.application.message_buffer.cache - # if message.get("to_address", "") == user] - messages = [message for message in self.application.message_buffer.cache] - tornado.log.logging.info("messages: %s", messages) - self.render("index.html", messages=messages, user=user) - -class MessageNewHandler(BaseHandler): - @tornado.web.authenticated - def post(self): - user = self.get_current_user() - message = { - "id": str(uuid.uuid4()), - "to_address": self.get_argument("to_address"), - "from_address": self.get_argument("from_address"), - "message_type": self.get_argument("message_type"), - "body": self.get_argument("body"), - "html": "", - } - # message["html"] contains itself: - message["html"] = tornado.escape.to_basestring( - self.render_string("message.html", message=message, user=user)) - ## Message goes to database: - self.application.handle_messages([message]) - if self.get_argument("next", None): - self.redirect(self.get_argument("next")) - else: - ## Sender gets the message back to handle: - self.write(message) - -class MessageUpdatesHandler(BaseHandler): - @tornado.web.authenticated - @gen.coroutine - def post(self): - ## send info to javascript: - cursor = self.get_argument("cursor", None) - # Save the future returned by wait_for_messages so we can cancel - # it in wait_for_messages - self.future = self.application.message_buffer.wait_for_messages(cursor=cursor) - messages = yield self.future - if self.request.connection.stream.closed(): - return - self.write(dict(messages=messages)) - - def on_connection_close(self): - self.application.message_buffer.cancel_wait(self.future) diff --git a/apps/tornado/app/key.py b/apps/tornado/app/key.py deleted file mode 100644 index 9bf320d..0000000 --- a/apps/tornado/app/key.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Any -from typing import Dict -from typing import Optional - -from Crypto.PublicKey import RSA - - -class Key(object): - DEFAULT_KEY_SIZE = 2048 - - def __init__(self, owner: str) -> None: - self.owner = owner - self.privkey_pem: Optional[str] = None - self.pubkey_pem: Optional[str] = None - self.privkey: Optional[Any] = None - self.pubkey: Optional[Any] = None - - def load_pub(self, pubkey_pem: str) -> None: - self.pubkey_pem = pubkey_pem - self.pubkey = RSA.importKey(pubkey_pem) - - def load(self, privkey_pem: str) -> None: - self.privkey_pem = privkey_pem - self.privkey = RSA.importKey(self.privkey_pem) - self.pubkey_pem = self.privkey.publickey().exportKey("PEM").decode("utf-8") - - def new(self) -> None: - k = RSA.generate(self.DEFAULT_KEY_SIZE) - self.privkey_pem = k.exportKey("PEM").decode("utf-8") - self.pubkey_pem = k.publickey().exportKey("PEM").decode("utf-8") - self.privkey = k - - def key_id(self) -> str: - return f"{self.owner}#main-key" - - def to_dict(self) -> Dict[str, Any]: - return { - "id": self.key_id(), - "owner": self.owner, - "publicKeyPem": self.pubkey_pem, - } diff --git a/apps/tornado/app/webfinger.py b/apps/tornado/app/webfinger.py deleted file mode 100644 index cf6f71a..0000000 --- a/apps/tornado/app/webfinger.py +++ /dev/null @@ -1,77 +0,0 @@ -import logging -from typing import Any -from typing import Dict -from typing import Optional -from urllib.parse import urlparse - -import requests - -from .activitypub import get_backend -from .urlutils import check_url - -logger = logging.getLogger(__name__) - - -def webfinger(resource: str) -> Optional[Dict[str, Any]]: - """Mastodon-like WebFinger resolution to retrieve the activity stream Actor URL. - """ - logger.info(f"performing webfinger resolution for {resource}") - protos = ["https", "http"] - if resource.startswith("http://"): - protos.reverse() - host = urlparse(resource).netloc - elif resource.startswith("https://"): - host = urlparse(resource).netloc - else: - if resource.startswith("acct:"): - resource = resource[5:] - if resource.startswith("@"): - resource = resource[1:] - _, host = resource.split("@", 1) - resource = "acct:" + resource - - # Security check on the url (like not calling localhost) - check_url(f"https://{host}") - - for i, proto in enumerate(protos): - try: - url = f"{proto}://{host}/.well-known/webfinger" - # FIXME(tsileo): BACKEND.fetch_json so we can set a UserAgent - resp = get_backend().fetch_json(url, params={"resource": resource}) - except requests.ConnectionError: - # If we tried https first and the domain is "http only" - if i == 0: - continue - break - if resp.status_code == 404: - return None - resp.raise_for_status() - return resp.json() - - -def get_remote_follow_template(resource: str) -> Optional[str]: - data = webfinger(resource) - if data is None: - return None - for link in data["links"]: - if link.get("rel") == "http://ostatus.org/schema/1.0/subscribe": - return link.get("template") - return None - - -def get_actor_url(resource: str) -> Optional[str]: - """Mastodon-like WebFinger resolution to retrieve the activity stream Actor URL. - - Returns: - the Actor URL or None if the resolution failed. - """ - data = webfinger(resource) - if data is None: - return None - for link in data["links"]: - if ( - link.get("rel") == "self" - and link.get("type") == "application/activity+json" - ): - return link.get("href") - return None diff --git a/apps/tornado/static/chat.css b/apps/tornado/static/chat.css deleted file mode 100644 index a85dd0c..0000000 --- a/apps/tornado/static/chat.css +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2009 FriendFeed - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain - * a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -body { - background: white; - margin: 10px; -} - -body, -input { - font-family: sans-serif; - font-size: 10pt; - color: black; -} - -table { - border-collapse: collapse; - border: 0; -} - -td { - border: 0; - padding: 5px; -} - -#body { - position: absolute; - bottom: 10px; - left: 10px; -} - -#input { - margin-top: 0.5em; -} - -#queue .incoming { - padding-top: 0.25em; - text-align: right; -} - -#queue .outgoing { - padding-top: 0.25em; - text-align: left; -} - -#nav { - float: right; - z-index: 99; -} diff --git a/apps/tornado/static/chat.js b/apps/tornado/static/chat.js deleted file mode 100644 index 2aa73f0..0000000 --- a/apps/tornado/static/chat.js +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2009 FriendFeed -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -$(document).ready(function() { - if (!window.console) window.console = {}; - if (!window.console.log) window.console.log = function() {}; - - $("#messageform").on("submit", function() { - newMessage($(this)); - return false; - }); - $("#messageform").on("keypress", function(e) { - if (e.keyCode == 13) { - newMessage($(this)); - return false; - } - return true; - }); - $("#message").select(); - updater.poll(); -}); - -function newMessage(form) { - console.log("newMessage!"); - var message = form.formToDict(); - var disabled = form.find("input[type=submit]"); - disabled.disable(); - $.postJSON("/message/new", message, function(response) { - // updater.showMessage(response, "outgoing"); - if (message.id) { - form.parent().remove(); - } else { - form.find("input[name=body]").val("").select(); - disabled.enable(); - } - }); -} - -function getCookie(name) { - var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); - return r ? r[1] : undefined; -} - -function getUser() { - return document.getElementById("from_address").value; -} - -jQuery.postJSON = function(url, args, callback) { - args._xsrf = getCookie("_xsrf"); - $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST", - success: function(response) { - if (callback) callback(eval("(" + response + ")")); - }, error: function(response) { - console.log("ERROR:", response); - }}); -}; - -jQuery.fn.formToDict = function() { - var fields = this.serializeArray(); - var json = {}; - for (var i = 0; i < fields.length; i++) { - json[fields[i].name] = fields[i].value; - } - if (json.next) delete json.next; - return json; -}; - -jQuery.fn.disable = function() { - this.enable(false); - return this; -}; - -jQuery.fn.enable = function(opt_enable) { - if (arguments.length && !opt_enable) { - this.attr("disabled", "disabled"); - } else { - this.removeAttr("disabled"); - } - return this; -}; - -var updater = { - errorSleepTime: 500, - cursor: null, - - poll: function() { - var args = {"_xsrf": getCookie("_xsrf")}; - if (updater.cursor) args.cursor = updater.cursor; - $.ajax({url: "/message/updates", type: "POST", dataType: "text", - data: $.param(args), success: updater.onSuccess, - error: updater.onError}); - }, - - onSuccess: function(response) { - try { - updater.newMessages(eval("(" + response + ")")); - } catch (e) { - updater.onError(); - return; - } - updater.errorSleepTime = 500; - window.setTimeout(updater.poll, 0); - }, - - onError: function(response) { - updater.errorSleepTime *= 2; - console.log("Poll error; sleeping for", updater.errorSleepTime, "ms"); - window.setTimeout(updater.poll, updater.errorSleepTime); - }, - - newMessages: function(response) { - console.log("newMessages!"); - if (!response.messages) return; - updater.cursor = response.cursor; - var messages = response.messages; - updater.cursor = messages[messages.length - 1].id; - console.log(messages.length, "new messages, cursor:", updater.cursor); - console.log("response:", response); - var user = getUser(); - for (var i = 0; i < messages.length; i++) { - if (messages[i].to_address === user || messages[i].from_address === user) { - if (messages[i].to_address === user) { - updater.showMessage(messages[i], "incoming"); - } else { - updater.showMessage(messages[i], "outgoing"); - } - } // else, don't show at all - } - }, - - showMessage: function(message, mailbox) { - console.log("showMessage", message); - var existing = $("#m" + message.id); - if (existing.length > 0) return; - var node = $(message.html); - node.hide(); - // make message; append it to queue - node.attr("class", mailbox); - console.log(node); - $("#queue").append(node); - node.slideDown(); - } -}; diff --git a/apps/tornado/templates/index.html b/apps/tornado/templates/index.html deleted file mode 100644 index 7a5befd..0000000 --- a/apps/tornado/templates/index.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - Tornado Chat Demo - - - - Logout -
-
- {% for message in messages %} - {% if message["to_address"] == user or message["from_address"] == user %} - {% module Template("message.html", message=message, user=user) %} - {% end %} - {% end %} -
-
-
- - - - - - - - -
To: - - - {% module xsrf_form_html() %} -
-
-
-
- - - - - diff --git a/apps/tornado/templates/login.html b/apps/tornado/templates/login.html deleted file mode 100644 index 8fceda5..0000000 --- a/apps/tornado/templates/login.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - Tornado Chat Demo - - - -

{{_("User Login")}}

-

{{_("Enter your login ID and password below.")}}

-
{% module xsrf_form_html() %} - - - - - - - - - - - - - -
- - - -
- - - -
- -

-
-
- - diff --git a/apps/tornado/templates/message.html b/apps/tornado/templates/message.html deleted file mode 100644 index 58b781e..0000000 --- a/apps/tornado/templates/message.html +++ /dev/null @@ -1,9 +0,0 @@ -{% if user == message["to_address"] %} -
-{% else %} -
-{% end %} -
To: {{ message["to_address"] }}
-
From: {{ message["from_address"] }}
-
{% module linkify(message["body"]) %}
-