From b1bd03ec8ecb38eda2b56d63b3e58e8d2a03e653 Mon Sep 17 00:00:00 2001 From: Blake DeMarcy Date: Wed, 12 Apr 2017 06:45:40 -0500 Subject: [PATCH] Public beta, code is becoming a dumpsterfire T_T --- clients/network_client.py | 25 ++++ clients/urwid/main.py | 234 +++++++++++++++++++++++++++++++++++--- server.py | 2 +- src/formatting.py | 85 ++++++++++---- 4 files changed, 309 insertions(+), 37 deletions(-) diff --git a/clients/network_client.py b/clients/network_client.py index 8becb5f..ac3a501 100644 --- a/clients/network_client.py +++ b/clients/network_client.py @@ -421,3 +421,28 @@ class BBJ(object): """ response = self("thread_load", format=format, thread_id=thread_id) return response["data"], response["usermap"] + + + def edit_query(self, thread_id, post_id): + """ + Queries ther server database to see if a post can + be edited by the logged in user. thread_id and + post_id are required. + + Returns a message object on success, or raises + a UserWarning describing why it failed. + """ + response = self("edit_query", thread_id=thread_id, post_id=int(post_id)) + return response["data"] + + + def can_edit(self, thread_id, post_id): + """ + Return bool True/False that the post at thread_id | post_id + can be edited by the logged in user. Will not raise UserWarning. + """ + try: + result = bool(self.edit_query(thread_id, post_id)) + except UserWarning: + result = False + return result diff --git a/clients/urwid/main.py b/clients/urwid/main.py index f1db278..edafd24 100644 --- a/clients/urwid/main.py +++ b/clients/urwid/main.py @@ -95,7 +95,7 @@ class App(object): ("default", "default", "default"), ("bar", "light magenta", "default"), ("button", "light red", "default"), - ("quote", "light green,underline", "default"), + ("quote", "brown", "default"), ("opt_prompt", "black", "light gray"), ("opt_header", "light cyan", "default"), ("hover", "light cyan", "default"), @@ -111,7 +111,15 @@ class App(object): ("3", "dark green", "default"), ("4", "dark blue", "default"), ("5", "dark cyan", "default"), - ("6", "dark magenta", "default") + ("6", "dark magenta", "default"), + + # and have the bolded colors use the same values times 10 + ("10", "light red", "default"), + ("20", "yellow", "default"), + ("30", "light green", "default"), + ("40", "light blue", "default"), + ("50", "light cyan", "default"), + ("60", "light magenta", "default") ] self.mode = None @@ -120,11 +128,12 @@ class App(object): self.prefs = bbjrc("load") self.window_split = False self.last_pos = 0 - + # self.jump_stack = [] self.walker = urwid.SimpleFocusListWalker([]) + self.box = ActionBox(self.walker) self.loop = urwid.MainLoop(urwid.Frame( urwid.LineBox( - ActionBox(self.walker), + self.box, title=self.prefs["frame_title"], **frame_theme() )), colors) @@ -244,6 +253,152 @@ class App(object): return "less than a minute ago" + def quote_view_action(self, button, post_id): + message = self.thread["messages"][post_id] + author = self.usermap[message["author"]] + color = str(author["color"]) + pid_string = ">>%d" % post_id + title = [ + ("button", pid_string), + ("default", " by "), + (color, "~" + author["user_name"]) + ] + widget = OptionsMenu( + urwid.ListBox( + urwid.SimpleFocusListWalker([ + *self.make_message_body(message) + ])), + title=pid_string, + **frame_theme() + ) + + self.loop.widget = urwid.Overlay( + widget, self.loop.widget, + align=("relative", 50), + valign=("relative", 50), + width=("relative", 98), + height=("relative", 60) + ) + + + + def quote_view_menu(self, button, post_ids): + if len(post_ids) == 1: + return self.quote_view_action(button, post_ids[0]) + # else: + # self.loop.widget = self.loop.widget[0] + buttons = [] + for pid in post_ids: + try: + message = self.thread["messages"][pid] + author = self.usermap[message["author"]] + color = str(author["color"]) + label = [ + ("button", ">>%d " % pid), + "(", (color, author["user_name"]), ")"] + except: + label = ("button", ">>%d") + buttons.append(cute_button(label, self.quote_view_action, pid)) + + widget = OptionsMenu( + urwid.ListBox(urwid.SimpleFocusListWalker(buttons)), + title="View a Quote", + **frame_theme() + ) + + self.loop.widget = urwid.Overlay( + widget, self.loop.widget, + align=("relative", 50), + valign=("relative", 50), + width=30, + height=len(buttons) + 3 + ) + + + def remove_overlays(self): + while True: + try: self.loop.widget = self.loop.widget[0] + except: break + + + def edit_post(self, button, message): + post_id = message["post_id"] + thread_id = message["thread_id"] + # first we need to get the server's version of the message + # instead of our formatted one + try: + message = network.edit_query(thread_id, post_id) + except UserWarning as e: + self.remove_overlays() + return self.temp_footer_message(e.description) + + self.loop.widget = urwid.Overlay( + urwid.LineBox( + ExternalEditor( + "edit_post", + init_body=message["body"], + post_id=post_id, + thread_id=thread_id), + title="[F1]Abort (save/quit to commit)", + **frame_theme()), + self.loop.widget, + align="center", + valign="middle", + width=("relative", 75), + height=("relative", 75)) + + + def reply(self, button, message): + self.remove_overlays() + self.compose(init_body=">>%d\n\n" % message["post_id"]) + + + + def on_post(self, button, message): + quotes = self.get_quotes(message) + author = self.usermap[message["author"]] + buttons = [ + urwid.Button("Reply", self.reply, message), + ] + + if quotes and message["post_id"] != 0: + buttons.insert(1, urwid.Button( + "View %sQuote" % ("a " if len(quotes) != 1 else ""), + self.quote_view_menu, quotes)) + + if network.can_edit(message["thread_id"], message["post_id"]): + buttons.insert(0, urwid.Button("Edit Post", self.edit_post, message)) + + widget = OptionsMenu( + urwid.ListBox(urwid.SimpleFocusListWalker(buttons)), + title=str(">>%d (%s)" % (message["post_id"], author["user_name"])), + **frame_theme() + ) + size = self.loop.screen_size + + self.loop.widget = urwid.Overlay( + urwid.AttrMap(widget, str(author["color"]*10)), + self.loop.widget, + align=("relative", 50), + valign=("relative", 50), + width=30, + height=len(buttons) + 3 + ) + + + def get_quotes(self, msg_object, value_type=int): + """ + Returns the post_ids that msg_object is quoting. + Is a list, may be empty. ids are ints by default + but can be passed `str` for strings. + """ + quotes = [] + for paragraph in msg_object["body"]: + # yes python is lisp fuck you + [quotes.append(cdr) for car, cdr in paragraph if car == "quote"] + return [value_type(q) for q in quotes] + + def make_message_body(self, message): """ Returns the widgets that comprise a message in a thread, including the @@ -256,9 +411,10 @@ class App(object): name = urwid.Text("~{}".format(self.usermap[message["author"]]["user_name"])) post = str(message["post_id"]) head = urwid.Columns([ - (2 + len(post), urwid.AttrMap(cute_button(">" + post), "button", "hover")), - (len(name._text) + 1, - urwid.AttrMap(name, str(self.usermap[message["author"]]["color"]))), + (2 + len(post), urwid.AttrMap( + cute_button(">" + post, self.on_post, message), "button", "hover")), + (len(name._text) + 1, urwid.AttrMap( + name, str(self.usermap[message["author"]]["color"]))), urwid.AttrMap(urwid.Text(info), "dim") ]) @@ -266,7 +422,9 @@ class App(object): return [ head, urwid.Divider(), - urwid.Columns([(self.prefs["max_text_width"], MessageBody(message["body"]))]), + urwid.Columns([ + (self.prefs["max_text_width"], MessageBody(message)) + ]), urwid.Divider(), urwid.AttrMap(urwid.Divider("-"), "dim") ] @@ -351,6 +509,7 @@ class App(object): def refresh(self, bottom=False): + self.remove_overlays() if self.mode == "index": return self.index() self.thread_load(None, self.thread["thread_id"]) @@ -518,6 +677,9 @@ class App(object): urwid.Button("Change password", on_press=self.change_password), urwid.Divider(), urwid.Text(("button", "Your color:")), + urwid.Text(("default", "This color will show on your " + "post headers and when people quote you.")), + urwid.Divider(), *user_colors ] else: @@ -651,14 +813,14 @@ class App(object): self.loop.stop() descriptor, path = tempfile.mkstemp() run("%s %s" % (self.prefs["editor"], path), shell=True) - with open(descriptor) as _: + with open(path) as _: body = _.read() os.remove(path) self.loop.start() return body.strip() - def compose(self, title=None): + def compose(self, title=None, init_body=""): """ Dispatches the appropriate composure mode and widget based on application context and user preferences. @@ -710,7 +872,10 @@ class App(object): urwid.BoxAdapter( urwid.AttrMap( urwid.LineBox( - ExternalEditor("thread_reply", thread_id=self.thread["thread_id"]), + ExternalEditor( + "thread_reply", + init_body=init_body, + thread_id=self.thread["thread_id"]), **frame_theme() ), "bar"), @@ -719,7 +884,11 @@ class App(object): class MessageBody(urwid.Text): - def __init__(self, text_objects): + """ + An urwid.Text object that works with the BBJ formatting directives. + """ + def __init__(self, message): + text_objects = message["body"] result = [] last_directive = None for paragraph in text_objects: @@ -738,7 +907,34 @@ class MessageBody(urwid.Text): result.append(("3", "%s\n" % body.strip())) elif directive == "quote": - result.append(("quote", ">>%s" % body)) + if message["post_id"] == 0: + # Quotes in OP have no meaning, just insert them plainly + result.append(("default", ">>%s" % body)) + continue + elif body == "0": + # quoting the OP, lets make it stand out a little + result.append(("50", ">>OP")) + continue + + color = "2" + try: + # we can get this quote by its index in the thread + message = app.thread["messages"][int(body)] + user = app.usermap[message["author"]] + # try to get the user's color, if its default use the normal one + _c = user["color"] + if _c != 0: + color = str(_c) + + if user != "anonymous" and user["user_name"] == network.user_name: + display = "[You]" + # bold it + color += "0" + else: + display = "[%s]" % user["user_name"] + except: # the quote may be garbage and refer to a nonexistant post + display = "" + result.append((color, ">>%s%s" % (body, display))) elif directive == "rainbow": color = 1 @@ -751,9 +947,8 @@ class MessageBody(urwid.Text): else: result.append(("default", body)) last_directive = directive - result.append("\n\n") - result.pop() + result.pop() # lazily ensure \n\n between paragraphs but not at the end super(MessageBody, self).__init__(result) @@ -820,6 +1015,13 @@ class FootPrompt(Prompt): class ExternalEditor(urwid.Terminal): def __init__(self, endpoint, **params): self.file_descriptor, self.path = tempfile.mkstemp() + with open(self.path, "w") as _: + if params.get("init_body"): + init_body = params.pop("init_body") + else: + init_body = "" + _.write(init_body) + self.endpoint = endpoint self.params = params env = os.environ @@ -836,7 +1038,7 @@ class ExternalEditor(urwid.Terminal): def keypress(self, size, key): if self.terminated: app.close_editor() - with open(self.file_descriptor) as _: + with open(self.path) as _: self.params.update({"body": _.read().strip()}) os.remove(self.path) if self.params["body"]: diff --git a/server.py b/server.py index 4311227..8129432 100644 --- a/server.py +++ b/server.py @@ -291,7 +291,7 @@ class API(object): (does not require a new body) Returns the original message object without any formatting - on success. + on success. Returns a descriptive code 4 otherwise. """ if user == db.anon: raise BBJUserError("Anons cannot edit messages.") diff --git a/src/formatting.py b/src/formatting.py index d0a64a3..acd7f63 100644 --- a/src/formatting.py +++ b/src/formatting.py @@ -1,22 +1,67 @@ """ -This module is not complete and none of its functions are currently -used elsewhere. Subject to major refactoring. +A B A N D O N ,: + A L L H O P E ,' | + / : + --' / + F O R Y E W H O \/ /:/ + E N T E R H E R E / ://_\ + __/ / + )'-. / + Crude hacks lie beneath us. ./ :\ + /.' ' +This module includes a couple '/' +of custom (GROAN) formatting + +specifications and parsers ' me irl +for them. Why did i do this? `. + I have no idea! .-"- + ( | + . .-' '. + ( (. )8: + .' / (_ ) + _. :(. )8P ` + . ( `-' ( `. . + . : ( .a8a) + /_`( "a `a. )"' + ( (/ . ' )==' + ( ( ) .8" + + (`'8a.( _( ( + ..-. `8P ) ` ) + + -' ( -ab: ) + ' _ ` (8P"Ya + _( ( )b -`. ) + + ( 8) ( _.aP" _a \( \ * ++ )/ (8P (88 ) ) + (a:f " `"` + + +The internal representation of formatted text is much like an s-expression. + +They are specified as follows: + [directive: this is the body of text to apply it to] + +The colon and the space following are important! The first space is not part +of the body, but any trailing spaces after it or at the end of the body are +included in the output. + +Escaping via backslash is supported. Nesting is supported as well, but escaping +the delimiters is a bit tricky when nesting (both ends need to be escaped). +See the following examples: + +[bold: this here \] is totally valid, and so is [<-TOTALLY OK this] +[bold: \[red: but both]<-CHOKE delimiters within a nest must be escaped.] + +Directives are only parsed whenever the directive name is defined, and the +colon/space follow it. Thus, including [brackets like this] in a post body +will NOT require you to escape it! Even [brackets: like this] is safe, because +brackets is not a defined formatting parameter. So, any amount of unescaped brackets +may exist within the body unless they mimic a directive. To escape a valid directive, +escaping only the opening is suffiecient: \[bold: like this]. The literal body of +text outputted by that will be [bold: like this], with the backslash removed. + +Just like the brackets themselves, backslashes may occur freely within bodies, +they are only removed when they occur before a valid expression. """ -test = """ -This is a small paragraph -thats divided between a -few rows. - -this opens a few linequotes. ->this is a few ->rows of ->sequential line breaks -and this is what follows right after -""" - -# from markdown import markdown -# from html import escape import re colors = [ @@ -28,16 +73,16 @@ markup = [ "bold", "italic", "underline", "linequote", "quote", "rainbow" ] +# PS: regex parsing is no longer used for these, preserving anyways # tokens being [red: this will be red] and [bold: this will be bold] # tokens = re.compile(r"\[(%s): (.+?)]" % "|".join(colors + markup), flags=re.DOTALL) +# linequotes being chan-style greentext, +# >like this +# linequotes = re.compile("^(>.+)$", flags=re.MULTILINE) # quotes being references to other post_ids, like >>34 or >>0 for OP quotes = re.compile(">>([0-9]+)") -# linequotes being chan-style greentext, -# >like this -linequotes = re.compile("^(>.+)$", flags=re.MULTILINE) - def parse_segments(text, sanitize_linequotes=True): """