kopia lustrzana https://github.com/dsblank/activitypub
Allow connection to any Redis db; rerenamed Dummy to List; refactor tables; fixed bug in Redis edit/delete items
rodzic
2921194244
commit
2c6cb1e48d
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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}]
|
|
@ -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)
|
||||
|
|
|
@ -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__()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -1,3 +0,0 @@
|
|||
from .app import main
|
||||
|
||||
main()
|
|
@ -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.")
|
|
@ -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
|
|
@ -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)
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Ładowanie…
Reference in New Issue