diff --git a/clients/network_client.py b/clients/network_client.py index 5f03f30..af51156 100644 --- a/clients/network_client.py +++ b/clients/network_client.py @@ -550,6 +550,21 @@ class BBJ(object): return response["data"] + def set_post_raw(self, thread_id, post_id, value): + """ + This is a subset of `edit_message` that retains the old + body and just sets its `send_raw` to your supplied `value`. + The `edited` parameter of the message on the server is not + modified. + """ + response = self( + "set_post_raw", + thread_id=thread_id, + post_id=post_id, + value=bool(value)) + return response["data"] + + def user_is_admin(self, user_name_or_id): """ Return boolean True or False whether the given user identifier diff --git a/clients/urwid/main.py b/clients/urwid/main.py index bf5e514..a6b4da0 100644 --- a/clients/urwid/main.py +++ b/clients/urwid/main.py @@ -500,6 +500,13 @@ class App(object): width=30, height=6) + def toggle_formatting(self, button, message): + self.remove_overlays() + raw = not message["send_raw"] + network.set_post_raw(message["thread_id"], message["post_id"], raw) + return self.refresh() + + def on_post(self, button, message): quotes = self.get_quotes(message) author = self.usermap[message["author"]] @@ -520,7 +527,11 @@ class App(object): msg = "Thread" else: msg = "Post" + raw = message["send_raw"] buttons.insert(0, urwid.Button("Delete %s" % msg, self.deletion_dialog, message)) + buttons.insert(0, urwid.Button( + "Enable Formatting" if raw else "Disable Formatting", + self.toggle_formatting, message)) buttons.insert(0, urwid.Button("Edit Post", self.edit_post, message)) if not buttons: @@ -550,6 +561,8 @@ class App(object): but can be passed `str` for strings. """ quotes = [] + if msg_object["send_raw"]: + return quotes for paragraph in msg_object["body"]: # yes python is lisp fuck you [quotes.append(cdr) for car, cdr in paragraph if car == "quote"] @@ -1297,6 +1310,9 @@ class MessageBody(urwid.Text): An urwid.Text object that works with the BBJ formatting directives. """ def __init__(self, message): + if message["send_raw"]: + return super(MessageBody, self).__init__(message["body"]) + text_objects = message["body"] result = [] last_directive = None diff --git a/schema.sql b/schema.sql index 79a4f5f..7bd61f2 100644 --- a/schema.sql +++ b/schema.sql @@ -32,5 +32,6 @@ create table messages ( author text, -- string (uuid1, user.user_id) created real, -- floating point unix timestamp (when reply was posted) edited int, -- bool - body text -- string + body text, -- string + send_raw int -- bool (1/true == never apply formatting) ); diff --git a/server.py b/server.py index eca942a..aa88cdd 100644 --- a/server.py +++ b/server.py @@ -214,11 +214,15 @@ class API(object): def thread_create(self, args, database, user, **kwargs): """ Creates a new thread and returns it. Requires the non-empty - string arguments `body` and `title` + 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. """ validate(args, ["body", "title"]) thread = db.thread_create( - database, user["user_id"], args["body"], args["title"]) + database, user["user_id"], args["body"], + args["title"], args.get("send_raw")) cherrypy.thread_data.usermap = \ create_usermap(database, thread["messages"]) return thread @@ -229,10 +233,14 @@ class API(object): """ Creates a new reply for the given thread and returns it. Requires the string arguments `thread_id` and `body` + + If the argument `send_raw` is specified and has a non-nil + value, the message will never recieve special formatting. """ validate(args, ["thread_id", "body"]) return db.thread_reply( - database, user["user_id"], args["thread_id"], args["body"]) + database, user["user_id"], args["thread_id"], + args["body"], args.get("send_raw")) @api_method @@ -266,13 +274,19 @@ class API(object): of a post without actually attempting to replace it, use `edit_query` first. + 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. + Returns the new message object. """ if user == db.anon: raise BBJUserError("Anons cannot edit messages.") validate(args, ["body", "thread_id", "post_id"]) return db.message_edit_commit( - database, user["user_id"], args["thread_id"], args["post_id"], args["body"]) + database, user["user_id"], args["thread_id"], + args["post_id"], args["body"], args.get("send_raw")) @api_method @@ -286,6 +300,8 @@ class API(object): or have admin rights. The same error descriptions and code are returned on falilure. Boolean true is returned on success. + + If the post_id is 0, the whole thread is deleted. """ if user == db.anon: raise BBJUserError("Anons cannot delete messages.") @@ -294,6 +310,31 @@ class API(object): database, user["user_id"], args["thread_id"], args["post_id"]) + @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. + """ + if user == db.anon: + raise BBJUserError("Anons cannot edit messages.") + 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) + @api_method def is_admin(self, args, database, user, **kwargs): @@ -343,17 +384,17 @@ class API(object): @api_method def set_thread_pin(self, args, database, user, **kwargs): """ - Requires the arguments `thread_id` and `pinned`. Pinned + Requires the arguments `thread_id` and `value`. `value` 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. - Returns the same boolean you supply as `pinned` + Returns the same boolean you supply as `value` """ - validate(args, ["thread_id", "pinned"]) + validate(args, ["thread_id", "value"]) if not user["is_admin"]: raise BBJUserError("Only admins can set thread pins") - return db.set_thread_pin(database, args["thread_id"], args["pinned"]) + return db.set_thread_pin(database, args["thread_id"], args["value"]) @api_method diff --git a/src/db.py b/src/db.py index 0c9920c..7fd0fd5 100644 --- a/src/db.py +++ b/src/db.py @@ -93,7 +93,7 @@ def thread_set_pin(connection, thread_id, pin_bool): return pin_bool -def thread_create(connection, author_id, body, title): +def thread_create(connection, author_id, body, title, send_raw=False): """ Create a new thread and return it. """ @@ -116,31 +116,32 @@ def thread_create(connection, author_id, body, title): # the thread is initially commited with reply_count -1 so that i can # just pass the message to the reply method, instead of duplicating # its code here. It then increments to 0. - thread_reply(connection, author_id, thread_id, body, time_override=now) + thread_reply(connection, author_id, thread_id, body, send_raw, time_override=now) # fetch the new thread out of the database instead of reusing the returned # objects, just to be 100% sure what is returned is what was committed return thread_get(connection, thread_id) -def thread_reply(connection, author_id, thread_id, body, time_override=None): +def thread_reply(connection, author_id, thread_id, body, send_raw=False, time_override=None): """ Submit a new reply for thread_id. Return the new reply object. - time_overide can be time() value to set as the new message time. + time_overide can be a time() value to set as the new message time. This is to keep post_id 0 in exact parity with its parent thread. """ validate([("body", body)]) now = time_override or time() thread = thread_get(connection, thread_id, messages=False) - count = thread["reply_count"] + 1 + thread["reply_count"] += 1 + count = thread["reply_count"] scheme = schema.message( thread_id, count, author_id, - now, False, body) + now, False, body, bool(send_raw)) connection.execute(""" INSERT INTO messages - VALUES (?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?) """, schema_values("message", scheme)) connection.execute(""" @@ -157,7 +158,7 @@ def thread_reply(connection, author_id, thread_id, body, time_override=None): def message_delete(connection, author, thread_id, post_id): """ 'Delete' a message from a thread. If the message being - deleted is an OP [pid 0], delete the whole thread. + deleted is an OP [post_id == 0], delete the whole thread. Requires an author id, the thread_id, and post_id. The same rules for edits apply to deletions: the same @@ -215,25 +216,58 @@ def message_edit_query(connection, author, thread_id, post_id): return message -def message_edit_commit(connection, author_id, thread_id, post_id, new_body): +def message_edit_commit( + connection, + author_id, + thread_id, + post_id, + new_body, + send_raw=None, + set_display=True): """ - Attempt to commit new_body to the existing message. Touches base with - message_edit_query first. Returns the newly updated message object. + Attempt to commit new_body, and optionally send_raw (default doesnt modify), + to the existing message. + + The send_raw and set_display paramter may be specified as the NoneType + to leave its old value intact. Otherwise its given value is coerced to + a boolean and is set on the message. send_raw when not explicitly specified + will keep its old value, while an unspecified set_display will set it to True. + + new_body may also be a NoneType to retain its old value. + + Touches base with message_edit_query first. Returns + the newly updated message object. """ - validate([("body", new_body)]) message = message_edit_query(connection, author_id, thread_id, post_id) - message["body"] = new_body - message["edited"] = True + + if new_body == None: + new_body = message["body"] + validate([("body", new_body)]) + + if send_raw == None: + send_raw = message["send_raw"] + else: + send_raw = bool(send_raw) + + if set_display == None: + display = message["edited"] + else: + display = bool(set_display) connection.execute(""" UPDATE messages SET body = ?, + send_raw = ?, edited = ? WHERE thread_id = ? AND post_id = ? - """, (new_body, True, thread_id, post_id)) - + """, (new_body, send_raw, display, thread_id, post_id)) connection.commit() + + message["body"] = new_body + message["send_raw"] = send_raw + message["edited"] = display + return message diff --git a/src/formatting.py b/src/formatting.py index a8f1375..0073140 100644 --- a/src/formatting.py +++ b/src/formatting.py @@ -180,7 +180,8 @@ def apply_formatting(msg_obj, formatter): documentation for each formatter. """ for x, obj in enumerate(msg_obj): - msg_obj[x]["body"] = formatter(obj["body"]) + if not msg_obj[x]["send_raw"]: + msg_obj[x]["body"] = formatter(obj["body"]) return msg_obj diff --git a/src/schema.py b/src/schema.py index b8d3504..7044c37 100644 --- a/src/schema.py +++ b/src/schema.py @@ -147,7 +147,8 @@ def message( author, # string (uuid1, user.user_id) created, # floating point unix timestamp (when reply was posted) edited, # bool - body): # string + body, # string + send_raw): # bool return { "thread_id": thread_id, @@ -155,5 +156,6 @@ def message( "author": author, "created": created, "edited": bool(edited), - "body": body + "body": body, + "send_raw": bool(send_raw) } diff --git a/src/utils.py b/src/utils.py index 20c8a0d..c533112 100644 --- a/src/utils.py +++ b/src/utils.py @@ -27,4 +27,4 @@ def schema_values(scheme, obj): elif scheme == "message": return ordered_keys(obj, "thread_id", "post_id", "author", - "created", "edited", "body") + "created", "edited", "body", "send_raw")