bbj/server.py

271 lines
7.9 KiB
Python
Raw Normal View History

2017-04-02 14:53:55 +00:00
from src.exceptions import BBJException, BBJParameterError, BBJUserError
from src import db, schema
2017-04-02 07:35:58 +00:00
from functools import wraps
2017-04-02 14:53:55 +00:00
from uuid import uuid1
import traceback
2017-04-02 07:35:58 +00:00
import cherrypy
import sqlite3
import json
dbname = "data.sqlite"
2017-04-02 07:35:58 +00:00
with sqlite3.connect(dbname) as _c:
db.anon_object = db.user_resolve(_c, "anonymous")
if not db.anon_object:
db.anon_object = db.user_register(_c, *db.anon_credentials)
2017-04-02 07:35:58 +00:00
# creates a database connection for each thread
def db_connect(_):
2017-04-02 07:35:58 +00:00
cherrypy.thread_data.db = sqlite3.connect(dbname)
cherrypy.engine.subscribe('start_thread', db_connect)
2017-04-02 07:35:58 +00:00
def api_method(function):
2017-04-02 07:35:58 +00:00
"""
A wrapper that handles encoding of objects and errors to a
standard format for the API, resolves and authorizes users
from header data, and prepares cherrypy.thread_data so other
funtions can handle the request.
In the body of each api method and all the functions
they utilize, BBJExceptions are caught and their attached
schema is dispatched to the client. All other unhandled
exceptions will throw a code 1 back at the client and log
2017-04-02 14:53:55 +00:00
it for inspection. Errors related to JSON decoding are
caught as well and returned to the client as code 0.
2017-04-02 07:35:58 +00:00
"""
@wraps(function)
def wrapper(*args, **kwargs):
response = None
try:
2017-04-02 14:53:55 +00:00
# read in the body from the request to a string...
body = str(cherrypy.request.body.read(), "utf8")
# is it empty? not all methods require an input
if body:
# if this fucks up, we throw code 0 instead of code 1
body = json.loads(body)
if isinstance(body, dict):
# lowercase all of its keys
body = {str(key).lower(): value for key, value
in body.items()}
cherrypy.request.json = body
else:
cherrypy.request.json = None
username = cherrypy.request.headers.get("User")
auth = cherrypy.request.headers.get("Auth")
anon = False
if not username and not auth:
user = db.anon_object
anon = True
2017-04-02 14:53:55 +00:00
elif not username or not auth:
2017-04-02 07:35:58 +00:00
return json.dumps(schema.error(5,
"User or Auth was given without the other."))
2017-04-02 07:35:58 +00:00
if not anon:
user = db.user_resolve(cherrypy.thread_data.db, username)
2017-04-02 14:53:55 +00:00
if not user:
raise BBJUserError("User %s is not registered" % username)
if auth != user["auth_hash"]:
return json.dumps(schema.error(5,
"Invalid authorization key for user."))
2017-04-02 07:35:58 +00:00
cherrypy.thread_data.user = user
cherrypy.thread_data.anon = anon
response = function(*args, **kwargs)
2017-04-02 07:35:58 +00:00
2017-04-02 14:53:55 +00:00
except json.JSONDecodeError as e:
response = schema.error(0, str(e))
2017-04-02 07:35:58 +00:00
except BBJException as e:
response = e.schema
2017-04-02 07:35:58 +00:00
except Exception as e:
2017-04-02 14:53:55 +00:00
error_id = uuid1().hex
response = schema.error(1,
"Internal server error: code {}. Tell ~desvox (the code too)"
.format(error_id))
with open("logs/exceptions/" + error_id, "a") as log:
traceback.print_tb(e.__traceback__, file=log)
log.write(repr(e))
finally:
return json.dumps(response)
2017-04-02 07:35:58 +00:00
return wrapper
def create_usermap(connection, obj):
"""
Creates a mapping of all the user_ids that occur in OBJ to
their full user objects (names, profile info, etc). Can
be a thread_index or a messages object from one.
"""
if isinstance(obj, dict):
# this is a message object for a thread, ditch the keys
obj = obj.values()
2017-04-02 07:35:58 +00:00
return {
user_id: db.user_resolve(
connection,
user_id,
externalize=True,
return_false=False)
for user_id in {
item["author"] for item in obj
}
}
def validate(json, args):
"""
Ensure the json object contains all the keys needed to satisfy
2017-04-02 14:53:55 +00:00
its endpoint (and isnt empty)
2017-04-02 07:35:58 +00:00
"""
2017-04-02 14:53:55 +00:00
if not json:
raise BBJParameterError(
"JSON input is empty. This method requires the following "
"arguments: {}".format(", ".join(args)))
2017-04-02 07:35:58 +00:00
for arg in args:
if arg not in json.keys():
raise BBJParameterError(
2017-04-02 14:53:55 +00:00
"Required parameter {} is absent from the request. "
"This method requires the following arguments: {}"
.format(arg, ", ".join(args)))
2017-04-02 07:35:58 +00:00
APICONFIG = {
"/": {
"tools.response_headers.on": True,
"tools.response_headers.headers": [
("Content-Type", "application/json")
],
}
}
class API(object):
@api_method
@cherrypy.expose
2017-04-02 14:53:55 +00:00
def get_me(self, *args, **kwargs):
"""
Requires no arguments. Returns your internal user object,
including your authorization hash.
"""
return schema.response(cherrypy.thread_data.user)
@api_method
@cherrypy.expose
2017-04-02 14:53:55 +00:00
def user_get(self, *args, **kwargs):
"""
Retreive an external user object for the given `user`.
Can be a user_id or user_name.
"""
args = cherrypy.request.json
2017-04-02 14:53:55 +00:00
validate(args, ["user"])
return schema.response(db.user_resolve(
cherrypy.thread_data.db,
args["user"],
return_false=False,
externalize=True))
@api_method
2017-04-02 07:35:58 +00:00
@cherrypy.expose
2017-04-02 14:53:55 +00:00
def thread_index(self, *args, **kwargs):
2017-04-02 07:35:58 +00:00
threads = db.thread_index(cherrypy.thread_data.db)
usermap = create_usermap(cherrypy.thread_data.db, threads)
return schema.response(threads, usermap)
2017-04-02 07:35:58 +00:00
@api_method
2017-04-02 07:35:58 +00:00
@cherrypy.expose
2017-04-02 14:53:55 +00:00
def thread_create(self, *args, **kwargs):
2017-04-02 07:35:58 +00:00
args = cherrypy.request.json
validate(args, ["body", "title"])
thread = db.thread_create(
cherrypy.thread_data.db,
cherrypy.thread_data.user["user_id"],
args["body"], args["title"])
usermap = {
cherrypy.thread_data.user["user_id"]:
cherrypy.thread_data.user
}
return schema.response(thread, usermap)
2017-04-02 07:35:58 +00:00
@api_method
2017-04-02 07:35:58 +00:00
@cherrypy.expose
2017-04-02 14:53:55 +00:00
def thread_reply(self, *args, **kwargs):
2017-04-02 07:35:58 +00:00
args = cherrypy.request.json
validate(args, ["thread_id", "body"])
return schema.response(db.thread_reply(
cherrypy.thread_data.db,
cherrypy.thread_data.user["user_id"],
args["thread_id"], args["body"]))
2017-04-02 07:35:58 +00:00
@api_method
2017-04-02 07:35:58 +00:00
@cherrypy.expose
2017-04-02 14:53:55 +00:00
def thread_load(self, *args, **kwargs):
2017-04-02 07:35:58 +00:00
args = cherrypy.request.json
validate(args, ["thread_id"])
thread = db.thread_get(
cherrypy.thread_data.db,
args["thread_id"])
usermap = create_usermap(
cherrypy.thread_data.db,
thread["messages"])
return schema.response(thread, usermap)
2017-04-02 07:35:58 +00:00
@api_method
2017-04-02 07:35:58 +00:00
@cherrypy.expose
2017-04-02 14:53:55 +00:00
def user_register(self, *args, **kwargs):
2017-04-02 07:35:58 +00:00
args = cherrypy.request.json
validate(args, ["user_name", "auth_hash"])
return schema.response(db.user_register(
cherrypy.thread_data.db,
args["user_name"],
args["auth_hash"]))
2017-04-02 07:35:58 +00:00
@api_method
2017-04-02 07:35:58 +00:00
@cherrypy.expose
2017-04-02 14:53:55 +00:00
def edit_query(self, *args, **kwargs):
2017-04-02 07:35:58 +00:00
args = cherrypy.request.json
validate(args, ["thread_id", "post_id"])
return schema.response(message_edit_query(
cherrypy.thread_data.db,
cherrypy.thread_data.user["user_id"],
args["thread_id"],
args["post_id"]))
2017-04-02 07:35:58 +00:00
2017-04-02 14:53:55 +00:00
@cherrypy.expose
def test(self, *args, **kwargs):
print(cherrypy.request.body.read())
return "{\"wow\": \"good job!\"}"
2017-04-02 07:35:58 +00:00
def run():
cherrypy.quickstart(API(), "/api")
if __name__ == "__main__":
print("wew")