2017-04-02 14:53:55 +00:00
|
|
|
from src.exceptions import BBJException, BBJParameterError, BBJUserError
|
2017-04-11 20:31:01 +00:00
|
|
|
from src import db, schema, formatting
|
2017-04-02 07:35:58 +00:00
|
|
|
from functools import wraps
|
2017-04-02 14:53:55 +00:00
|
|
|
from uuid import uuid1
|
2017-04-13 07:08:19 +00:00
|
|
|
from sys import argv
|
2017-04-02 14:53:55 +00:00
|
|
|
import traceback
|
2017-04-02 07:35:58 +00:00
|
|
|
import cherrypy
|
|
|
|
import sqlite3
|
|
|
|
import json
|
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
dbname = "data.sqlite"
|
2017-05-01 08:13:36 +00:00
|
|
|
|
|
|
|
# any values here may be overrided in the config.json. Any values not listed
|
|
|
|
# here will have no effect on the server.
|
2018-08-06 00:13:41 +00:00
|
|
|
default_config = {
|
|
|
|
"admins": [],
|
2017-05-01 08:13:36 +00:00
|
|
|
"port": 7099,
|
|
|
|
"host": "127.0.0.1",
|
|
|
|
"instance_name": "BBJ",
|
|
|
|
"allow_anon": True,
|
|
|
|
"debug": False
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
2018-08-06 00:13:41 +00:00
|
|
|
with open("config.json", "r") as _in:
|
|
|
|
app_config = json.load(_in)
|
|
|
|
# update the file with new keys if necessary
|
|
|
|
for key, default_value in default_config.items():
|
|
|
|
# The application will never store a config value
|
|
|
|
# as the NoneType, so users may set an option as
|
|
|
|
# null in their file to reset it to default
|
|
|
|
if key not in app_config or app_config[key] == None:
|
|
|
|
app_config[key] = default_value
|
|
|
|
# else just use the defaults
|
2017-05-01 08:13:36 +00:00
|
|
|
except FileNotFoundError:
|
2018-08-06 00:13:41 +00:00
|
|
|
app_config = default_prefs
|
|
|
|
finally:
|
|
|
|
with open("config.json", "w") as _out:
|
|
|
|
json.dump(app_config, _out, indent=2)
|
2017-05-01 08:13:36 +00:00
|
|
|
|
2017-04-02 19:26:49 +00:00
|
|
|
|
2017-04-02 08:34:52 +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
|
2017-04-03 04:23:19 +00:00
|
|
|
from header data, and prepares the arguments for each method.
|
2017-04-02 08:34:52 +00:00
|
|
|
|
|
|
|
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
|
|
|
"""
|
2017-04-03 04:23:19 +00:00
|
|
|
function.exposed = True
|
2017-05-01 08:13:36 +00:00
|
|
|
|
2017-04-02 07:35:58 +00:00
|
|
|
@wraps(function)
|
2017-04-03 04:23:19 +00:00
|
|
|
def wrapper(self, *args, **kwargs):
|
2017-04-02 08:34:52 +00:00
|
|
|
response = None
|
2017-05-01 08:13:36 +00:00
|
|
|
debug = app_config["debug"]
|
2017-04-02 08:34:52 +00:00
|
|
|
try:
|
2017-04-03 08:31:00 +00:00
|
|
|
connection = sqlite3.connect(dbname)
|
2017-04-02 14:53:55 +00:00
|
|
|
# read in the body from the request to a string...
|
2017-04-03 16:00:17 +00:00
|
|
|
if cherrypy.request.method == "POST":
|
2017-05-03 22:33:41 +00:00
|
|
|
read_in = str(cherrypy.request.body.read(), "utf8")
|
|
|
|
if not read_in:
|
|
|
|
# the body may be empty, not all methods require input
|
|
|
|
body = {}
|
|
|
|
else:
|
|
|
|
body = json.loads(read_in)
|
|
|
|
if not isinstance(body, dict):
|
|
|
|
raise BBJParameterError("Non-JSONObject input")
|
2017-04-03 04:23:19 +00:00
|
|
|
# lowercase all of its top-level keys
|
2017-04-03 08:52:51 +00:00
|
|
|
body = {key.lower(): value for key, value in body.items()}
|
2017-05-03 22:33:41 +00:00
|
|
|
else:
|
|
|
|
body = {}
|
2017-04-02 14:53:55 +00:00
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
username = cherrypy.request.headers.get("User")
|
|
|
|
auth = cherrypy.request.headers.get("Auth")
|
|
|
|
|
2017-04-02 19:26:49 +00:00
|
|
|
if (username and not auth) or (auth and not username):
|
2017-05-01 08:13:36 +00:00
|
|
|
raise BBJParameterError(
|
|
|
|
"User or Auth was given without the other.")
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-02 19:26:49 +00:00
|
|
|
elif not username and not auth:
|
|
|
|
user = db.anon
|
|
|
|
|
|
|
|
else:
|
2017-04-03 08:31:00 +00:00
|
|
|
user = db.user_resolve(connection, username)
|
2017-04-02 14:53:55 +00:00
|
|
|
if not user:
|
|
|
|
raise BBJUserError("User %s is not registered" % username)
|
|
|
|
|
2017-04-28 00:31:19 +00:00
|
|
|
elif auth.lower() != user["auth_hash"].lower():
|
2017-05-01 08:13:36 +00:00
|
|
|
raise BBJException(
|
|
|
|
5, "Invalid authorization key for user.")
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-02 19:26:49 +00:00
|
|
|
# api_methods may choose to bind a usermap into the thread_data
|
|
|
|
# which will send it off with the response
|
|
|
|
cherrypy.thread_data.usermap = {}
|
2017-04-03 08:31:00 +00:00
|
|
|
value = function(self, body, connection, user)
|
2017-04-02 19:26:49 +00:00
|
|
|
response = schema.response(value, cherrypy.thread_data.usermap)
|
2017-04-02 14:53:55 +00:00
|
|
|
|
2017-04-02 07:35:58 +00:00
|
|
|
except BBJException as e:
|
2017-04-02 08:34:52 +00:00
|
|
|
response = e.schema
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-02 19:26:49 +00:00
|
|
|
except json.JSONDecodeError as e:
|
|
|
|
response = schema.error(0, str(e))
|
|
|
|
|
2017-04-02 07:35:58 +00:00
|
|
|
except Exception as e:
|
2017-04-02 14:53:55 +00:00
|
|
|
error_id = uuid1().hex
|
2017-05-01 08:13:36 +00:00
|
|
|
response = schema.error(
|
|
|
|
1, "Internal server error: code {} {}".format(
|
|
|
|
error_id, repr(e)))
|
2017-04-02 14:53:55 +00:00
|
|
|
with open("logs/exceptions/" + error_id, "a") as log:
|
|
|
|
traceback.print_tb(e.__traceback__, file=log)
|
|
|
|
log.write(repr(e))
|
2017-04-02 19:26:49 +00:00
|
|
|
print("logged code 1 exception " + error_id)
|
2017-04-02 08:34:52 +00:00
|
|
|
|
|
|
|
finally:
|
2017-04-03 08:31:00 +00:00
|
|
|
connection.close()
|
2017-04-02 08:34:52 +00:00
|
|
|
return json.dumps(response)
|
2017-04-02 07:35:58 +00:00
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
2017-04-25 08:36:51 +00:00
|
|
|
def create_usermap(connection, obj, index=False):
|
2017-04-02 07:35:58 +00:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
2017-04-25 08:36:51 +00:00
|
|
|
user_set = {item["author"] for item in obj}
|
|
|
|
if index:
|
|
|
|
[user_set.add(item["last_author"]) for item in obj]
|
2017-04-02 07:35:58 +00:00
|
|
|
return {
|
|
|
|
user_id: db.user_resolve(
|
|
|
|
connection,
|
|
|
|
user_id,
|
|
|
|
externalize=True,
|
|
|
|
return_false=False)
|
2017-04-25 08:36:51 +00:00
|
|
|
for user_id in user_set
|
2017-04-02 07:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-04-26 20:35:54 +00:00
|
|
|
def do_formatting(format_spec, messages):
|
|
|
|
if not format_spec:
|
|
|
|
return None
|
|
|
|
|
|
|
|
elif format_spec == "sequential":
|
|
|
|
method = formatting.sequential_expressions
|
|
|
|
|
|
|
|
else:
|
|
|
|
raise BBJParameterError("invalid formatter specification")
|
|
|
|
|
|
|
|
formatting.apply_formatting(messages, method)
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2017-04-02 07:35:58 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
2017-05-01 08:13:36 +00:00
|
|
|
def no_anon_hook(user, message=None, user_error=True):
|
|
|
|
if user is db.anon:
|
|
|
|
exception = BBJUserError if user_error else BBJParameterError
|
|
|
|
if message:
|
|
|
|
raise exception(message)
|
|
|
|
elif not app_config["allow_anon"]:
|
|
|
|
raise exception(
|
|
|
|
"Anonymous participation has been disabled on this instance.")
|
|
|
|
|
|
|
|
|
2017-04-02 07:35:58 +00:00
|
|
|
class API(object):
|
2017-04-03 04:23:19 +00:00
|
|
|
"""
|
2017-05-04 02:35:47 +00:00
|
|
|
This object contains all the API endpoints for bbj. The html serving
|
|
|
|
part of the server is not written yet, so this is currently the only
|
|
|
|
module being served.
|
|
|
|
|
|
|
|
The docstrings below are specifically formatted for the mkdocs static
|
|
|
|
site generator. The ugly `doctype` and `arglist` attributes assigned
|
|
|
|
after each method definition are for use in the `mkendpoints.py` script.
|
2017-04-03 04:23:19 +00:00
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
|
2018-08-04 13:21:01 +00:00
|
|
|
@api_method
|
|
|
|
def instance_info(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Return configuration info for this running instance of the BBJ server.
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
"allow_anon": app_config["allow_anon"],
|
2018-08-06 00:13:41 +00:00
|
|
|
"instance_name": app_config["instance_name"],
|
|
|
|
"admins": app_config["admins"]
|
2018-08-04 13:21:01 +00:00
|
|
|
}
|
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
@api_method
|
2017-04-02 19:26:49 +00:00
|
|
|
def user_register(self, args, database, user, **kwargs):
|
|
|
|
"""
|
2017-05-04 02:35:47 +00:00
|
|
|
Register a new user into the system and return the new user object
|
|
|
|
on success. The returned object includes the same `user_name` and
|
|
|
|
`auth_hash` that you supply, in addition to all the default user
|
|
|
|
parameters. Returns code 4 errors for any failures.
|
2017-04-02 19:26:49 +00:00
|
|
|
"""
|
|
|
|
validate(args, ["user_name", "auth_hash"])
|
|
|
|
return db.user_register(
|
|
|
|
database, args["user_name"], args["auth_hash"])
|
2017-05-02 22:06:02 +00:00
|
|
|
user_register.doctype = "Users"
|
2017-05-04 00:37:36 +00:00
|
|
|
user_register.arglist = (
|
2017-05-02 22:06:02 +00:00
|
|
|
("user_name", "string: the desired display name"),
|
|
|
|
("auth_hash", "string: a sha256 hash of a password")
|
2017-05-04 00:37:36 +00:00
|
|
|
)
|
2017-04-02 19:26:49 +00:00
|
|
|
|
|
|
|
@api_method
|
|
|
|
def user_update(self, args, database, user, **kwargs):
|
|
|
|
"""
|
2017-05-04 02:35:47 +00:00
|
|
|
Receives new parameters and assigns them to the user object.
|
|
|
|
This method requires that you send a valid User/Auth header
|
|
|
|
pair with your request, and the changes are made to that
|
|
|
|
account.
|
|
|
|
|
|
|
|
Take care to keep your client's User/Auth header pair up to date
|
|
|
|
after using this method.
|
2017-04-02 19:26:49 +00:00
|
|
|
|
2017-05-04 02:35:47 +00:00
|
|
|
The newly updated user object is returned on success,
|
|
|
|
including the `auth_hash`.
|
2017-04-02 19:26:49 +00:00
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
no_anon_hook(user, "Anons cannot modify their account.")
|
|
|
|
validate(args, []) # just make sure its not empty
|
2017-04-02 19:26:49 +00:00
|
|
|
return db.user_update(database, user, args)
|
2017-05-02 22:06:02 +00:00
|
|
|
user_update.doctype = "Users"
|
2017-05-04 00:37:36 +00:00
|
|
|
user_update.arglist = (
|
2017-05-04 02:35:47 +00:00
|
|
|
("Any of the following may be submitted", ""),
|
2017-05-02 22:06:02 +00:00
|
|
|
("user_name", "string: a desired display name"),
|
|
|
|
("auth_hash", "string: sha256 hash for a new password"),
|
|
|
|
("quip", "string: a short string that can be used as a signature"),
|
|
|
|
("bio", "string: a user biography for their profile"),
|
|
|
|
("color", "integer: 0-6, a display color for the user")
|
2017-05-04 00:37:36 +00:00
|
|
|
)
|
2017-04-02 19:26:49 +00:00
|
|
|
|
|
|
|
@api_method
|
|
|
|
def get_me(self, args, database, user, **kwargs):
|
2017-04-02 08:34:52 +00:00
|
|
|
"""
|
|
|
|
Requires no arguments. Returns your internal user object,
|
2017-05-04 23:05:24 +00:00
|
|
|
including your `auth_hash`.
|
2017-04-02 08:34:52 +00:00
|
|
|
"""
|
2017-04-02 19:26:49 +00:00
|
|
|
return user
|
2017-05-02 22:06:02 +00:00
|
|
|
get_me.doctype = "Users"
|
2017-05-04 00:37:36 +00:00
|
|
|
get_me.arglist = (("", ""),)
|
2017-04-02 19:26:49 +00:00
|
|
|
|
2017-04-28 00:07:18 +00:00
|
|
|
@api_method
|
|
|
|
def user_map(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Returns an array with all registered user_ids, with the usermap
|
2017-05-04 23:05:24 +00:00
|
|
|
object populated by their full objects. This method is _NEVER_
|
|
|
|
neccesary when using other endpoints, as the usermap returned
|
|
|
|
on those requests already contains all the information you will
|
|
|
|
need. This endpoint is useful for statistic purposes only.
|
2017-04-28 00:07:18 +00:00
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
users = {
|
|
|
|
user[0] for user in database.execute("SELECT user_id FROM users")
|
|
|
|
}
|
2017-04-28 00:07:18 +00:00
|
|
|
cherrypy.thread_data.usermap = {
|
|
|
|
user: db.user_resolve(
|
|
|
|
database,
|
|
|
|
user,
|
|
|
|
externalize=True,
|
|
|
|
return_false=False)
|
|
|
|
for user in users
|
|
|
|
}
|
|
|
|
return list(users)
|
2017-05-02 22:06:02 +00:00
|
|
|
user_map.doctype = "Tools"
|
2017-05-04 00:37:36 +00:00
|
|
|
user_map.arglist = (("", ""),)
|
2017-04-28 00:07:18 +00:00
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
@api_method
|
2017-04-02 19:26:49 +00:00
|
|
|
def user_get(self, args, database, user, **kwargs):
|
2017-04-02 08:34:52 +00:00
|
|
|
"""
|
2017-05-04 23:05:24 +00:00
|
|
|
Returns a user object for the given target.
|
2017-04-02 08:34:52 +00:00
|
|
|
"""
|
2017-05-02 22:06:02 +00:00
|
|
|
validate(args, ["target_user"])
|
2017-04-02 19:26:49 +00:00
|
|
|
return db.user_resolve(
|
2017-05-02 22:06:02 +00:00
|
|
|
database, args["target_user"], return_false=False, externalize=True)
|
|
|
|
user_get.doctype = "Users"
|
2017-05-04 00:37:36 +00:00
|
|
|
user_get.arglist = (
|
|
|
|
("target_user", "string: either a user_name or a user_id"),
|
|
|
|
)
|
2017-04-02 08:34:52 +00:00
|
|
|
|
2017-04-03 16:00:17 +00:00
|
|
|
@api_method
|
|
|
|
def user_is_registered(self, args, database, user, **kwargs):
|
|
|
|
"""
|
2017-05-04 23:05:24 +00:00
|
|
|
Returns boolean `true` or `false` of whether the given target is
|
|
|
|
registered on the server.
|
2017-04-03 16:00:17 +00:00
|
|
|
"""
|
|
|
|
validate(args, ["target_user"])
|
|
|
|
return bool(db.user_resolve(database, args["target_user"]))
|
2017-05-02 22:06:02 +00:00
|
|
|
user_is_registered.doctype = "Users"
|
2017-05-04 00:37:36 +00:00
|
|
|
user_is_registered.arglist = (
|
|
|
|
("target_user", "string: either a user_name or a user_id"),
|
|
|
|
)
|
2017-04-03 16:00:17 +00:00
|
|
|
|
|
|
|
@api_method
|
|
|
|
def check_auth(self, args, database, user, **kwargs):
|
|
|
|
"""
|
2017-05-04 23:05:24 +00:00
|
|
|
Returns boolean `true` or `false` of whether the hash given
|
|
|
|
is correct for the given user.
|
2017-04-03 16:00:17 +00:00
|
|
|
"""
|
|
|
|
validate(args, ["target_user", "target_hash"])
|
2017-05-01 08:13:36 +00:00
|
|
|
user = db.user_resolve(
|
|
|
|
database, args["target_user"], return_false=False)
|
2017-04-28 00:31:19 +00:00
|
|
|
return args["target_hash"].lower() == user["auth_hash"].lower()
|
2017-05-02 22:06:02 +00:00
|
|
|
check_auth.doctype = "Authorization"
|
2017-05-04 00:37:36 +00:00
|
|
|
check_auth.arglist = (
|
|
|
|
("target_user", "string: either a user_name or a user_id"),
|
|
|
|
("target_hash", "string: sha256 hash for the password to check")
|
|
|
|
)
|
2017-04-03 16:00:17 +00:00
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
@api_method
|
2017-04-02 19:26:49 +00:00
|
|
|
def thread_index(self, args, database, user, **kwargs):
|
|
|
|
"""
|
2017-05-04 23:05:24 +00:00
|
|
|
Return an array with all the server's threads. They are already sorted for
|
|
|
|
you; most recently modified threads are at the beginning of the array.
|
|
|
|
Unless you supply `include_op`, these threads have no `messages` parameter.
|
|
|
|
If you do, the `messages` parameter is an array with a single message object
|
|
|
|
for the original post.
|
2017-04-02 19:26:49 +00:00
|
|
|
"""
|
2017-05-03 22:35:02 +00:00
|
|
|
threads = db.thread_index(database, include_op=args.get("include_op"))
|
2017-04-25 08:36:51 +00:00
|
|
|
cherrypy.thread_data.usermap = create_usermap(database, threads, True)
|
2017-04-02 19:26:49 +00:00
|
|
|
return threads
|
2017-05-02 22:06:02 +00:00
|
|
|
thread_index.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
thread_index.arglist = (
|
2017-05-04 23:05:24 +00:00
|
|
|
("OPTIONAL: include_op", "boolean: Include a `messages` object containing the original post"),
|
2017-05-04 00:37:36 +00:00
|
|
|
)
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-26 19:51:03 +00:00
|
|
|
@api_method
|
|
|
|
def message_feed(self, args, database, user, **kwargs):
|
|
|
|
"""
|
2017-05-04 23:05:24 +00:00
|
|
|
Returns a special object representing all activity on the board since `time`.
|
2017-04-26 19:51:03 +00:00
|
|
|
|
2017-05-04 23:05:24 +00:00
|
|
|
```javascript
|
2017-04-26 19:51:03 +00:00
|
|
|
{
|
|
|
|
"threads": {
|
|
|
|
"thread_id": {
|
2017-05-04 23:05:24 +00:00
|
|
|
// ...thread object
|
2017-04-26 19:51:03 +00:00
|
|
|
},
|
2017-05-04 23:05:24 +00:00
|
|
|
// ...more thread_id/object pairs
|
2017-04-26 19:51:03 +00:00
|
|
|
},
|
2017-05-04 23:05:24 +00:00
|
|
|
"messages": [
|
|
|
|
...standard message object array sorted by date
|
|
|
|
]
|
2017-04-26 19:51:03 +00:00
|
|
|
}
|
2017-05-04 23:05:24 +00:00
|
|
|
```
|
2017-04-26 19:51:03 +00:00
|
|
|
|
2017-05-04 23:05:24 +00:00
|
|
|
The message objects in `messages` are the same objects returned
|
2017-04-26 19:51:03 +00:00
|
|
|
in threads normally. They each have a thread_id parameter, and
|
2017-05-04 23:05:24 +00:00
|
|
|
you can access metadata for these threads by the `threads` object
|
2017-04-26 19:51:03 +00:00
|
|
|
which is also provided.
|
|
|
|
|
2017-05-04 23:05:24 +00:00
|
|
|
The `messages` array is already sorted by submission time, newest
|
2017-04-26 19:51:03 +00:00
|
|
|
first. The order in the threads object is undefined and you should
|
|
|
|
instead use their `last_mod` attribute if you intend to list them
|
|
|
|
out visually.
|
|
|
|
"""
|
2017-05-04 23:05:24 +00:00
|
|
|
# XXX: Update with new formatting documentation for arg `format`
|
2017-04-26 19:51:03 +00:00
|
|
|
validate(args, ["time"])
|
2017-04-26 20:35:54 +00:00
|
|
|
feed = db.message_feed(database, args["time"])
|
|
|
|
|
|
|
|
_map = create_usermap(database, feed["messages"])
|
|
|
|
_map.update(create_usermap(database, feed["threads"].values(), True))
|
|
|
|
cherrypy.thread_data.usermap.update(_map)
|
2017-04-26 19:51:03 +00:00
|
|
|
|
2017-04-26 20:35:54 +00:00
|
|
|
do_formatting(args.get("format"), feed["messages"])
|
|
|
|
return feed
|
2017-05-02 22:06:02 +00:00
|
|
|
message_feed.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
message_feed.arglist = (
|
|
|
|
("time", "int/float: epoch/unix time of the earliest point of interest"),
|
2017-05-04 23:05:24 +00:00
|
|
|
("OPTIONAL: format", "string: the specifier for the desired formatting engine")
|
2017-05-04 00:37:36 +00:00
|
|
|
)
|
2017-04-26 19:51:03 +00:00
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
@api_method
|
2017-04-02 19:26:49 +00:00
|
|
|
def thread_create(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Creates a new thread and returns it. Requires the non-empty
|
2017-04-18 17:17:03 +00:00
|
|
|
string arguments `body` and `title`.
|
|
|
|
|
|
|
|
If the argument `send_raw` is specified and has a non-nil
|
|
|
|
value, the OP message will never recieve special formatting.
|
2017-04-02 19:26:49 +00:00
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
no_anon_hook(user)
|
2017-04-02 07:35:58 +00:00
|
|
|
validate(args, ["body", "title"])
|
|
|
|
thread = db.thread_create(
|
2017-04-18 17:17:03 +00:00
|
|
|
database, user["user_id"], args["body"],
|
2017-05-04 23:22:10 +00:00
|
|
|
args["title"], args.get("send_raw"))
|
2017-04-03 08:31:00 +00:00
|
|
|
cherrypy.thread_data.usermap = \
|
|
|
|
create_usermap(database, thread["messages"])
|
2017-04-02 19:26:49 +00:00
|
|
|
return thread
|
2017-05-02 22:06:02 +00:00
|
|
|
thread_create.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
thread_create.arglist = (
|
|
|
|
("body", "string: The body of the first message"),
|
|
|
|
("title", "string: The title name for this thread"),
|
|
|
|
("OPTIONAL: send_raw", "boolean: formatting mode for the first message.")
|
|
|
|
)
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
@api_method
|
2017-04-02 19:26:49 +00:00
|
|
|
def thread_reply(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Creates a new reply for the given thread and returns it.
|
|
|
|
Requires the string arguments `thread_id` and `body`
|
2017-04-18 17:17:03 +00:00
|
|
|
|
|
|
|
If the argument `send_raw` is specified and has a non-nil
|
|
|
|
value, the message will never recieve special formatting.
|
2017-04-02 19:26:49 +00:00
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
no_anon_hook(user)
|
2017-04-02 07:35:58 +00:00
|
|
|
validate(args, ["thread_id", "body"])
|
2017-04-02 19:26:49 +00:00
|
|
|
return db.thread_reply(
|
2017-04-18 17:17:03 +00:00
|
|
|
database, user["user_id"], args["thread_id"],
|
2017-05-04 23:22:10 +00:00
|
|
|
args["body"], args.get("send_raw"))
|
2017-05-02 22:06:02 +00:00
|
|
|
thread_reply.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
thread_reply.arglist = (
|
|
|
|
("thread_id", "string: the id for the thread this message should post to."),
|
|
|
|
("body", "string: the message's body of text."),
|
|
|
|
("OPTIONAL: send_raw", "boolean: formatting mode for the posted message.")
|
|
|
|
)
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
@api_method
|
2017-04-02 19:26:49 +00:00
|
|
|
def thread_load(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Returns the thread object with all of its messages loaded.
|
2017-04-12 14:09:16 +00:00
|
|
|
Requires the argument `thread_id`. `format` may also be
|
|
|
|
specified as a formatter to run the messages through.
|
|
|
|
Currently only "sequential" is supported.
|
2017-04-30 01:48:38 +00:00
|
|
|
|
|
|
|
You may also supply the parameter `op_only`. When it's value
|
|
|
|
is non-nil, the messages array will only include post_id 0 (the first)
|
2017-04-02 19:26:49 +00:00
|
|
|
"""
|
2017-04-02 07:35:58 +00:00
|
|
|
validate(args, ["thread_id"])
|
2017-04-30 01:48:38 +00:00
|
|
|
thread = db.thread_get(
|
|
|
|
database, args["thread_id"], op_only=args.get("op_only"))
|
2017-04-02 19:26:49 +00:00
|
|
|
cherrypy.thread_data.usermap = \
|
|
|
|
create_usermap(database, thread["messages"])
|
2017-04-26 20:35:54 +00:00
|
|
|
do_formatting(args.get("format"), thread["messages"])
|
2017-04-02 19:26:49 +00:00
|
|
|
return thread
|
2017-05-02 22:06:02 +00:00
|
|
|
thread_load.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
thread_load.arglist = (
|
|
|
|
("thread_id", "string: the thread to load."),
|
|
|
|
("OPTIONAL: op_only", "boolean: include only the original message in `messages`"),
|
|
|
|
# XXX formal formatting documentation is desperately needed
|
|
|
|
("OPTIONAL: format", "string: the formatting type of the returned messages.")
|
|
|
|
)
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
@api_method
|
2017-04-02 19:26:49 +00:00
|
|
|
def edit_post(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Replace a post with a new body. Requires the arguments
|
|
|
|
`thread_id`, `post_id`, and `body`. This method verifies
|
|
|
|
that the user can edit a post before commiting the change,
|
|
|
|
otherwise an error object is returned whose description
|
|
|
|
should be shown to the user.
|
|
|
|
|
2017-04-03 04:23:19 +00:00
|
|
|
To perform sanity checks and retrieve the unformatted body
|
|
|
|
of a post without actually attempting to replace it, use
|
|
|
|
`edit_query` first.
|
2017-04-02 19:26:49 +00:00
|
|
|
|
2017-04-18 17:17:03 +00:00
|
|
|
Optionally you may also include the argument `send_raw` to
|
|
|
|
set the message's formatting flag. However, if this is the
|
|
|
|
only change you would like to make, you should use the
|
|
|
|
endpoint `set_post_raw` instead.
|
|
|
|
|
2017-04-02 19:26:49 +00:00
|
|
|
Returns the new message object.
|
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
no_anon_hook(user, "Anons cannot edit messages.")
|
2017-04-02 19:26:49 +00:00
|
|
|
validate(args, ["body", "thread_id", "post_id"])
|
2017-04-03 08:31:00 +00:00
|
|
|
return db.message_edit_commit(
|
2017-04-18 17:17:03 +00:00
|
|
|
database, user["user_id"], args["thread_id"],
|
|
|
|
args["post_id"], args["body"], args.get("send_raw"))
|
2017-05-02 22:06:02 +00:00
|
|
|
edit_post.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
edit_post.arglist = (
|
|
|
|
("thread_id", "string: the thread the message was posted in."),
|
|
|
|
("post_id", "integer: the target post_id to edit."),
|
|
|
|
("body", "string: the new message body."),
|
|
|
|
("OPTIONAL: send_raw", "boolean: set the formatting mode for the target message.")
|
|
|
|
)
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-13 16:49:20 +00:00
|
|
|
@api_method
|
|
|
|
def delete_post(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Requires the arguments `thread_id` and `post_id`.
|
|
|
|
|
|
|
|
Delete a message from a thread. The same rules apply
|
|
|
|
here as `edit_post` and `edit_query`: the logged in user
|
|
|
|
must either be the one who posted the message within 24hrs,
|
|
|
|
or have admin rights. The same error descriptions and code
|
|
|
|
are returned on falilure. Boolean true is returned on
|
|
|
|
success.
|
2017-04-18 17:17:03 +00:00
|
|
|
|
|
|
|
If the post_id is 0, the whole thread is deleted.
|
2017-04-13 16:49:20 +00:00
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
no_anon_hook(user, "Anons cannot delete messages.")
|
2017-04-13 16:49:20 +00:00
|
|
|
validate(args, ["thread_id", "post_id"])
|
|
|
|
return db.message_delete(
|
|
|
|
database, user["user_id"], args["thread_id"], args["post_id"])
|
2017-05-02 22:06:02 +00:00
|
|
|
delete_post.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
delete_post.arglist = (
|
|
|
|
("thread_id", "string: the id of the thread this message was posted in."),
|
|
|
|
("post_id", "integer: the id of the target message.")
|
|
|
|
)
|
2017-04-13 16:49:20 +00:00
|
|
|
|
2017-04-18 17:17:03 +00:00
|
|
|
@api_method
|
|
|
|
def set_post_raw(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Requires the boolean argument of `value`, string argument
|
|
|
|
`thread_id`, and integer argument `post_id`. `value`, when false,
|
|
|
|
means that the message will be passed through message formatters
|
|
|
|
before being sent to clients. When `value` is true, this means
|
|
|
|
it will never go through formatters, all of its whitespace is
|
|
|
|
sent to clients verbatim and expressions are not processed.
|
|
|
|
|
|
|
|
The same rules for editing messages (see `edit_query`) apply here
|
|
|
|
and the same error objects are returned for violations.
|
|
|
|
|
|
|
|
You may optionally set this value as well when using `edit_post`,
|
|
|
|
but if this is the only change you want to make to the message,
|
|
|
|
using this endpoint instead is preferable.
|
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
no_anon_hook(user, "Anons cannot edit messages.")
|
2017-04-18 17:17:03 +00:00
|
|
|
validate(args, ["value", "thread_id", "post_id"])
|
|
|
|
return db.message_edit_commit(
|
|
|
|
database, user["user_id"],
|
|
|
|
args["thread_id"], args["post_id"],
|
|
|
|
None, args["value"], None)
|
2017-05-02 22:06:02 +00:00
|
|
|
set_post_raw.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
set_post_raw.arglist = (
|
|
|
|
("thread_id", "string: the id of the thread the message was posted in."),
|
|
|
|
("post_id", "integer: the id of the target message."),
|
|
|
|
("value", "boolean: the new `send_raw` value to apply to the message.")
|
|
|
|
)
|
2017-04-18 17:17:03 +00:00
|
|
|
|
2017-04-03 16:00:17 +00:00
|
|
|
@api_method
|
|
|
|
def is_admin(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Requires the argument `target_user`. Returns a boolean
|
|
|
|
of whether that user is an admin.
|
|
|
|
"""
|
|
|
|
validate(args, ["target_user"])
|
2017-05-01 08:13:36 +00:00
|
|
|
user = db.user_resolve(
|
|
|
|
database, args["target_user"], return_false=False)
|
2017-04-03 16:00:17 +00:00
|
|
|
return user["is_admin"]
|
2017-05-02 22:06:02 +00:00
|
|
|
is_admin.doctype = "Users"
|
2017-05-04 00:37:36 +00:00
|
|
|
is_admin.arglist = (
|
|
|
|
("target_user", "string: user_id or user_name to check against."),
|
|
|
|
)
|
2017-04-03 16:00:17 +00:00
|
|
|
|
2017-04-02 08:34:52 +00:00
|
|
|
@api_method
|
2017-04-02 19:26:49 +00:00
|
|
|
def edit_query(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Queries the database to ensure the user can edit a given
|
|
|
|
message. Requires the arguments `thread_id` and `post_id`
|
|
|
|
(does not require a new body)
|
|
|
|
|
2017-04-03 04:23:19 +00:00
|
|
|
Returns the original message object without any formatting
|
2017-04-12 11:45:40 +00:00
|
|
|
on success. Returns a descriptive code 4 otherwise.
|
2017-04-02 19:26:49 +00:00
|
|
|
"""
|
2017-05-01 08:13:36 +00:00
|
|
|
no_anon_hook(user, "Anons cannot edit messages.")
|
2017-04-02 07:35:58 +00:00
|
|
|
validate(args, ["thread_id", "post_id"])
|
2017-04-03 08:31:00 +00:00
|
|
|
return db.message_edit_query(
|
2017-04-02 19:26:49 +00:00
|
|
|
database, user["user_id"], args["thread_id"], args["post_id"])
|
2017-05-02 22:06:02 +00:00
|
|
|
edit_query.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
edit_query.arglist = (
|
|
|
|
("thread_id", "string: the id of the thread the message was posted in."),
|
|
|
|
("post_id", "integer: the id of the target message.")
|
|
|
|
)
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-12 14:09:16 +00:00
|
|
|
@api_method
|
|
|
|
def format_message(self, args, database, user, **kwargs):
|
|
|
|
"""
|
|
|
|
Requires the arguments `body` and `format`. Applies
|
|
|
|
`format` to `body` and returns the new object. See
|
|
|
|
`thread_load` for supported specifications for `format`.
|
|
|
|
"""
|
|
|
|
validate(args, ["format", "body"])
|
|
|
|
message = [{"body": args["body"]}]
|
2017-04-26 20:35:54 +00:00
|
|
|
do_formatting(args["format"], message)
|
2017-04-12 14:09:16 +00:00
|
|
|
return message[0]["body"]
|
2017-05-02 22:06:02 +00:00
|
|
|
format_message.doctype = "Tools"
|
2017-05-04 00:37:36 +00:00
|
|
|
format_message.arglist = (
|
|
|
|
("body", "string: the message body to apply formatting to."),
|
|
|
|
# XXX: remember to update this with new formatting docs
|
|
|
|
("format", "string: the specifier for the desired formatting engine")
|
|
|
|
)
|
2017-04-12 14:09:16 +00:00
|
|
|
|
2017-04-03 16:00:17 +00:00
|
|
|
@api_method
|
2017-04-08 07:27:05 +00:00
|
|
|
def set_thread_pin(self, args, database, user, **kwargs):
|
|
|
|
"""
|
2017-04-18 17:17:03 +00:00
|
|
|
Requires the arguments `thread_id` and `value`. `value`
|
2017-04-08 07:27:05 +00:00
|
|
|
must be a boolean of what the pinned status should be.
|
|
|
|
This method requires that the caller is logged in and
|
|
|
|
has admin status on their account.
|
|
|
|
|
2017-04-18 17:17:03 +00:00
|
|
|
Returns the same boolean you supply as `value`
|
2017-04-08 07:27:05 +00:00
|
|
|
"""
|
2017-04-18 17:17:03 +00:00
|
|
|
validate(args, ["thread_id", "value"])
|
2017-04-08 07:27:05 +00:00
|
|
|
if not user["is_admin"]:
|
|
|
|
raise BBJUserError("Only admins can set thread pins")
|
2017-04-18 17:17:03 +00:00
|
|
|
return db.set_thread_pin(database, args["thread_id"], args["value"])
|
2017-05-02 22:06:02 +00:00
|
|
|
set_thread_pin.doctype = "Threads & Messages"
|
2017-05-04 00:37:36 +00:00
|
|
|
set_thread_pin.arglist = (
|
|
|
|
("thread_id", "string: the id of the thread to modify."),
|
|
|
|
("value", "boolean: `true` to pin thread, `false` otherwise.")
|
|
|
|
)
|
2017-04-08 07:27:05 +00:00
|
|
|
|
|
|
|
@api_method
|
|
|
|
def db_validate(self, args, database, user, **kwargs):
|
2017-04-03 16:00:17 +00:00
|
|
|
"""
|
2017-09-03 05:27:45 +00:00
|
|
|
See also [the Input Validation page](validation.md).
|
|
|
|
|
2017-04-03 16:00:17 +00:00
|
|
|
Requires the arguments `key` and `value`. Returns an object
|
|
|
|
with information about the database sanity criteria for
|
|
|
|
key. This can be used to validate user input in the client
|
|
|
|
before trying to send it to the server.
|
|
|
|
|
|
|
|
If the argument `error` is supplied with a non-nil value,
|
|
|
|
the server will return a standard error object on failure
|
|
|
|
instead of the special object described below.
|
|
|
|
|
|
|
|
The returned object has two keys:
|
|
|
|
|
|
|
|
{
|
|
|
|
"bool": true/false,
|
|
|
|
"description": null/"why this value is bad"
|
|
|
|
}
|
|
|
|
|
|
|
|
If bool == false, description is a string describing the
|
|
|
|
problem. If bool == true, description is null and the
|
|
|
|
provided value is safe to use.
|
|
|
|
"""
|
|
|
|
validate(args, ["key", "value"])
|
|
|
|
response = dict()
|
|
|
|
try:
|
|
|
|
db.validate([(args["key"], args["value"])])
|
|
|
|
response["bool"] = True
|
|
|
|
response["description"] = None
|
|
|
|
except BBJException as e:
|
|
|
|
if args.get("error"):
|
|
|
|
raise
|
|
|
|
response["bool"] = False
|
|
|
|
response["description"] = e.description
|
|
|
|
return response
|
2017-05-02 22:06:02 +00:00
|
|
|
db_validate.doctype = "Tools"
|
2017-05-04 00:37:36 +00:00
|
|
|
db_validate.arglist = (
|
|
|
|
("key", "string: the identifier for the ruleset to check."),
|
|
|
|
("value", "VARIES: the object for which `key` will check for."),
|
|
|
|
("OPTIONAL: error", "boolean: when `true`, will return an API error "
|
|
|
|
"response instead of a special object.")
|
|
|
|
)
|
2017-04-03 16:00:17 +00:00
|
|
|
|
|
|
|
|
2017-04-04 09:51:37 +00:00
|
|
|
def api_http_error(status, message, traceback, version):
|
2017-05-01 08:13:36 +00:00
|
|
|
return json.dumps(schema.error(
|
|
|
|
2, "HTTP error {}: {}".format(status, message)))
|
2017-04-04 09:51:37 +00:00
|
|
|
|
|
|
|
|
2017-05-01 08:13:36 +00:00
|
|
|
API_CONFIG = {
|
2017-04-04 09:51:37 +00:00
|
|
|
"/": {
|
|
|
|
"error_page.default": api_http_error
|
|
|
|
}
|
|
|
|
}
|
2017-04-03 08:34:50 +00:00
|
|
|
|
2017-04-02 07:35:58 +00:00
|
|
|
|
|
|
|
def run():
|
2017-04-04 09:51:37 +00:00
|
|
|
_c = sqlite3.connect(dbname)
|
|
|
|
try:
|
2018-08-06 00:13:41 +00:00
|
|
|
db.set_admins(_c, app_config["admins"])
|
|
|
|
# user anonymity is achieved in the laziest possible way: a literal user
|
|
|
|
# named anonymous. may god have mercy on my soul.
|
2017-04-04 09:51:37 +00:00
|
|
|
db.anon = db.user_resolve(_c, "anonymous")
|
|
|
|
if not db.anon:
|
|
|
|
db.anon = db.user_register(
|
2017-05-01 08:13:36 +00:00
|
|
|
_c, "anonymous", # this is the hash for "anon"
|
2017-04-04 09:51:37 +00:00
|
|
|
"5430eeed859cad61d925097ec4f53246"
|
|
|
|
"1ccf1ab6b9802b09a313be1478a4d614")
|
|
|
|
finally:
|
|
|
|
_c.close()
|
2017-05-01 08:13:36 +00:00
|
|
|
cherrypy.quickstart(API(), "/api", API_CONFIG)
|
2017-04-02 07:35:58 +00:00
|
|
|
|
2017-04-13 07:08:19 +00:00
|
|
|
|
2017-05-01 08:13:36 +00:00
|
|
|
def get_arg(key, default, get_value=True):
|
2017-04-27 03:08:24 +00:00
|
|
|
try:
|
2017-05-01 08:13:36 +00:00
|
|
|
spec = argv.index("--" + key)
|
|
|
|
value = argv[spec + 1] if get_value else True
|
|
|
|
except ValueError: # --key not specified
|
|
|
|
value = default
|
|
|
|
except IndexError: # flag given but no value
|
|
|
|
exit("invalid format for --" + key)
|
|
|
|
return value
|
2017-04-27 03:08:24 +00:00
|
|
|
|
2017-04-28 00:07:18 +00:00
|
|
|
|
2017-05-01 08:13:36 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
port = get_arg("port", app_config["port"])
|
|
|
|
host = get_arg("host", app_config["host"])
|
|
|
|
debug = get_arg("debug", app_config["debug"], False)
|
2017-04-27 03:08:24 +00:00
|
|
|
cherrypy.config.update({
|
|
|
|
"server.socket_port": int(port),
|
|
|
|
"server.socket_host": host
|
|
|
|
})
|
2017-04-13 07:08:19 +00:00
|
|
|
run()
|