Allow connection to any Redis db; rerenamed Dummy to List; refactor tables; fixed bug in Redis edit/delete items

master
Douglas Blank 2018-07-23 11:48:40 -04:00
rodzic 2921194244
commit 2c6cb1e48d
21 zmienionych plików z 248 dodań i 988 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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]

Wyświetl plik

@ -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': '<p>2</p>', '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}]

Wyświetl plik

@ -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)

Wyświetl plik

@ -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__()

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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()

Wyświetl plik

@ -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()

Wyświetl plik

@ -1,3 +0,0 @@
from .app import main
main()

Wyświetl plik

@ -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.")

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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,
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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();
}
};

Wyświetl plik

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tornado Chat Demo</title>
<link rel="stylesheet" href="{{ static_url("chat.css") }}" type="text/css">
</head>
<body>
<a href="/logout">Logout</a>
<div id="body">
<div id="queue">
{% for message in messages %}
{% if message["to_address"] == user or message["from_address"] == user %}
{% module Template("message.html", message=message, user=user) %}
{% end %}
{% end %}
</div>
<div id="input">
<form action="/message/new" method="post" id="messageform">
<table>
<tr>
<td>To: <input type="text" name="to_address" id="to_address" value=""></td>
<td><input type="hidden" name="from_address" id="from_address" value="{{ user }}"></td>
<td><input type="hidden" name="message_type" id="message_type" value="Note"></td>
<td><input type="text" name="body" id="message" style="width:100px"></td>
<td>
<input type="submit" value="{{ _("Post") }}">
<input type="hidden" name="next" value="{{ request.path }}">
{% module xsrf_form_html() %}
</td>
</tr>
</table>
</form>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js" type="text/javascript"></script>
<script src="{{ static_url("chat.js") }}" type="text/javascript"></script>
<script type="text/javascript">
<!-- // required to refresh the page after navigating away, and coming back:
if (!!window.performance && window.performance.navigation.type === 2) {
console.log('Reloading');
window.location.reload();
}
//-->
</script>
</body>
</html>

Wyświetl plik

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tornado Chat Demo</title>
<link rel="stylesheet" href="{{ static_url("chat.css") }}" type="text/css">
</head>
<body>
<h2>{{_("User Login")}}</h2>
<p id="description">{{_("Enter your login ID and password below.")}}</p>
<form method="POST" action="/login">{% module xsrf_form_html() %}
<table>
<tr>
<td align="right">
<label for="id_username">{{_("Username")}}: </label>
</td>
<td>
<input class="get_focus" type="text" name="username" />
</td>
</tr>
<tr>
<td align="right">
<label for="id_password">{{_("Password")}}: </label>
</td>
<td>
<input type="password" name="password" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<p><input type="submit" value="Login" style="width: 155px;"/></p>
</td>
</tr>
</table>
</form>
</body>
</html>

Wyświetl plik

@ -1,9 +0,0 @@
{% if user == message["to_address"] %}
<div class="incoming">
{% else %}
<div class="outgoing">
{% end %}
<div> <b>To</b>: {{ message["to_address"] }} </div>
<div> <b>From</b>: {{ message["from_address"] }} </div>
<div> {% module linkify(message["body"]) %} </div>
</div>