aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorBlake DeMarcy <ofunknowndescent@gmail.com>2017-03-03 16:26:59 -0600
committerBlake DeMarcy <ofunknowndescent@gmail.com>2017-03-03 16:26:59 -0600
commite892d20e18a5a7002071f7ec0468437227e508a7 (patch)
tree21359d49dffdfa9cd71067ef919f41e1c8e053ff /src
parenta9634b9aea64eb4e00c497e72814f6bd9c237556 (diff)
downloadbbj-e892d20e18a5a7002071f7ec0468437227e508a7.tar.gz
primitive message editing support; new sanity checks
Diffstat (limited to 'src')
-rw-r--r--src/db.py127
-rw-r--r--src/endpoints.py103
-rw-r--r--src/server.py7
3 files changed, 217 insertions, 20 deletions
diff --git a/src/db.py b/src/db.py
index c7f2e46..0eb38d7 100644
--- a/src/db.py
+++ b/src/db.py
@@ -23,6 +23,7 @@ except FileNotFoundError:
f.write(json.dumps(USERDB))
path.os.chmod(path.join(PATH, "userdb"), 0o600)
+
### THREAD MANAGEMENT ###
def thread_index(key="lastmod", markup=True):
@@ -36,8 +37,10 @@ def thread_index(key="lastmod", markup=True):
def thread_create(author, body, title, tags):
ID = uuid1().hex
- # make sure None, False, and empty arrays are always repped consistently
- tags = tags if tags else []
+ if tags:
+ tags = [tag.strip() for tag in tags.split(",")]
+ else: # make sure None, False, and empty arrays are always repped consistently
+ tags = []
scheme = schema.thread(ID, author, body, title, tags)
thread_dump(ID, scheme)
return scheme
@@ -46,14 +49,7 @@ def thread_create(author, body, title, tags):
def thread_load(ID, markup=True):
try:
with open(path.join(PATH, "threads", ID), "r") as f:
- thread = json.loads(f.read())
- if not markup:
- thread["body"] = formatting.cleanse(thread["body"])
- for x in range(len(thread["replies"])):
- thread["replies"][x]["body"] = formatting.cleanse(
- thread["replies"][x]["body"])
- return thread
-
+ return json.loads(f.read())
except FileNotFoundError:
return False
@@ -82,6 +78,44 @@ def thread_reply(ID, author, body):
return reply
+def index_reply(reply_list, post_id):
+ for index, reply in enumerate(reply_list):
+ if reply["post_id"] == post_id:
+ return index
+ else:
+ raise IndexError
+
+
+def edit_handler(json, thread=None):
+ try:
+ target_id = json["post_id"]
+ if not thread:
+ thread = thread_load(json["thread_id"])
+ if not thread:
+ return False, schema.error(7, "Requested thread does not exist.")
+
+
+ if target_id == 1:
+ target = thread
+ else:
+ target = thread["replies"][
+ index_reply(thread["replies"], target_id)]
+
+ if not user_is_admin(json["user"]):
+ if json["user"] != target["author"]:
+ return False, schema.error(10,
+ "non-admin attempt to edit another user's message")
+
+ elif (time() - target["created"]) > 86400:
+ return False, schema.error(9,
+ "message is too old to edit (24hr limit)")
+
+ return True, target
+
+ except IndexError:
+ return False, schema.error(3, "post_id out of bounds for requested thread")
+
+
### USER MANAGEMENT ###
def user_dbdump(dictionary):
@@ -104,6 +138,15 @@ def user_register(auth_hash, name, quip, bio):
if USERDB["namemap"].get(name):
return schema.error(4, "Username taken.")
+ for ok, error in [
+ user_namecheck(name),
+ user_authcheck(auth_hash),
+ user_quipcheck(quip),
+ user_biocheck(bio)]:
+
+ if not ok:
+ return error
+
ID = uuid1().hex
scheme = schema.user_internal(ID, auth_hash, name, quip, bio, False)
USERDB.update({ID: scheme})
@@ -123,6 +166,70 @@ def user_auth(ID, auth_hash):
return auth_hash == USERDB[ID]["auth_hash"]
+def user_is_admin(ID):
+ return USERDB[ID]["admin"]
+
+
def user_update(ID, **params):
USERDB[ID].update(params)
return USERDB[ID]
+
+
+### SANITY CHECKS ###
+
+def contains_nonspaces(string):
+ return any([char in string for char in "\t\n\r\x0b\x0c"])
+
+
+def user_namecheck(name):
+ if not name:
+ return False, schema.error(4,
+ "Username may not be empty.")
+
+ elif contains_nonspaces(name):
+ return False, schema.error(4,
+ "Username cannot contain whitespace chars besides spaces.")
+
+ elif len(username) > 24:
+ return False, schema.error(4,
+ "Username is too long (max 24 chars)")
+
+ return True, True
+
+
+def user_authcheck(auth_hash):
+ if not auth_hash:
+ return schema.error(3,
+ "auth_hash may not be empty")
+
+ elif len(auth_hash) != 64:
+ return False, schema.error(4,
+ "Client error: invalid SHA-256 hash.")
+
+ return True, True
+
+
+def user_quipcheck(quip):
+ if not quip:
+ return ""
+
+ elif contains_nonspaces(quip):
+ return False, schema.error(4,
+ "Quip cannot contain whitespace chars besides spaces.")
+
+ elif len(quip) > 120:
+ return False, schema.error(4,
+ "Quip is too long (max 120 chars)")
+
+ return True, True
+
+
+def user_biocheck(bio):
+ if not bio:
+ return ""
+
+ elif len(bio) > 4096:
+ return False, schema.error(4,
+ "Bio is too long (max 4096 chars)")
+
+ return True, True
diff --git a/src/endpoints.py b/src/endpoints.py
index 3042627..434f791 100644
--- a/src/endpoints.py
+++ b/src/endpoints.py
@@ -1,17 +1,22 @@
from src import formatting
from src import schema
+from time import time
from src import db
-from json import dumps
+
endpoints = {
- "check_auth": ["user", "auth_hash"],
- "is_registered": ["target_user"],
- "thread_load": ["thread_id"],
- "thread_index": [],
- "thread_create": ["title", "body", "tags"],
- "thread_reply": ["thread_id", "body"],
- "user_register": ["user", "auth_hash", "quip", "bio"],
- "user_get": ["target_user"],
+ "check_auth": ["user", "auth_hash"],
+ "is_registered": ["target_user"],
+ "is_admin": ["target_user"],
+ "thread_index": [],
+ "thread_load": ["thread_id"],
+ "thread_create": ["title", "body", "tags"],
+ "thread_reply": ["thread_id", "body"],
+ "edit_post": ["thread_id", "post_id", "body"],
+ "can_edit": ["thread_id", "post_id"],
+ "user_register": ["user", "auth_hash", "quip", "bio"],
+ "user_get": ["target_user"],
+ "user_name_to_id": ["target_user"]
}
@@ -21,6 +26,8 @@ authless = [
]
+# this is not actually an endpoint, but produces a required
+# element of thread responses.
def create_usermap(thread, index=False):
if index:
return {user: db.user_get(user) for user in
@@ -31,23 +38,81 @@ def create_usermap(thread, index=False):
return {x: db.user_get(x) for x in result}
+def user_name_to_id(json):
+ """
+ Returns a string of the target_user's ID when it is
+ part of the database: a non-existent user will return
+ a boolean false.
+ """
+ return db.user_resolve(json["target_user"])
+
+
def is_registered(json):
+ """
+ Returns true or false whether target_user is registered
+ in the system. This function only takes usernames: not
+ user IDs.
+ """
return bool(db.USERDB["namemap"].get(json["target_user"]))
def check_auth(json):
+ "Returns true or false whether auth_hashes matches user."
return bool(db.user_auth(json["user"], json["auth_hash"]))
+def is_admin(json):
+ """
+ Returns true or false whether target_user is a system
+ administrator. Takes a username or user ID. Nonexistent
+ users return false.
+ """
+ user = db.user_resolve(json["target_user"])
+ if user:
+ return db.user_is_admin(user)
+ return False
+
+
def user_register(json):
+ """
+ Registers a new user into the system. Returns the new internal user
+ object on success, or an error response.
+
+ auth_hash should be a hexadecimal SHA-256 string, produced from a
+ UTF-8 password string.
+
+ user should be a string containing no newlines and
+ under 24 characters in length.
+
+ quip is a string, up to 120 characters, provided by the user
+ the acts as small bio, suitable for display next to posts
+ if the client wants to. Whitespace characters besides space
+ are not allowed. The string may be empty.
+
+ bio is a string, up to 4096 chars, provided by the user that
+ can be shown on profiles. There are no character type limits
+ for this entry. The string may be empty.
+
+ All errors for this endpoint with code 4 should show the
+ description direcrtly to the user.
+
+ """
+
return schema.response(
db.user_register(
- json["auth_hash"],
json["user"],
+ json["auth_hash"],
json["quip"],
json["bio"]))
+def user_get(json):
+ user = db.user_resolve(json["target_user"])
+ if not user:
+ return False
+ return db.user_get(user)
+
+
def thread_index(json):
index = db.thread_index(markup=not json.get("nomarkup"))
return schema.response({"threads": index}, create_usermap(index, True))
@@ -79,3 +144,21 @@ def thread_reply(json):
if json.get("nomarkup"):
reply["body"] = formatting.cleanse(reply["body"])
return schema.response(reply)
+
+
+def can_edit(json):
+ return db.edit_handler(json)[0]
+
+
+def edit_post(json):
+ thread = db.thread_load(json["thread_id"])
+ admin = db.user_is_admin(json["user"])
+ target_id = json["post_id"]
+ query, obj = db.edit_handler(json, thread)
+
+ if query:
+ obj["body"] = json["body"]
+ obj["lastmod"] = time()
+ obj["edited"] = True
+ db.thread_dump(json["thread_id"], thread)
+ return obj
diff --git a/src/server.py b/src/server.py
index f5bf305..345cc68 100644
--- a/src/server.py
+++ b/src/server.py
@@ -43,6 +43,13 @@ class RequestHandler(StreamRequestHandler):
db.user_auth(user, request.get("auth_hash")):
return self.reply(schema.error(6, "Authorization failed."))
+ # post_ids are always returned as integers, but for callers who
+ # provide them as something else, try to convert them.
+ if isinstance(request.get("post_id"), (float, str)):
+ try: request["post_id"] = int(request["post_id"])
+ except Exception:
+ return schema.error(3, "Non-numeric post_id")
+
# exception handling is now passed to the endpoints;
# anything unhandled beyond here is a code 1
self.reply(eval("endpoints." + endpoint)(request))
Un proyecto texto-plano.xyz