fake messages, formatting endpoint, new help menus

pull/4/head
Blake DeMarcy 2017-04-12 09:09:16 -05:00
parent 7eef803084
commit 09077baeac
4 changed files with 221 additions and 19 deletions

View File

@ -1,6 +1,7 @@
from urllib.error import URLError from urllib.error import URLError
import urllib.request as url import urllib.request as url
from hashlib import sha256 from hashlib import sha256
from time import time
import json import json
@ -423,6 +424,32 @@ class BBJ(object):
return response["data"], response["usermap"] return response["data"], response["usermap"]
def fake_message(self, body="!!", format="sequential", author=None, post_id=0):
"""
Produce a a valid message object with `body`. Useful for
testing and can also be used mimic server messages in a
client.
"""
return {
"body": self.format_message(body, format),
"author": author or self.user["user_id"],
"post_id": post_id,
"created": time(),
"edited": False,
"thread_id": "gibberish"
}
def format_message(self, body, format="sequential"):
"""
Send `body` to the server to be formatted according to `format`,
defaulting to the sequential parser. Returns the body object.
"""
response = self("format_message", body=body, format=format)
return response["data"]
def edit_query(self, thread_id, post_id): def edit_query(self, thread_id, post_id):
""" """
Queries ther server database to see if a post can Queries ther server database to see if a post can

View File

@ -20,7 +20,6 @@ Please mail me (~desvox) for feedback and for any of your
"OH MY GOD WHY WOULD YOU DO THIS"'s or "PEP8 IS A THING"'s. "OH MY GOD WHY WOULD YOU DO THIS"'s or "PEP8 IS A THING"'s.
""" """
from network import BBJ, URLError from network import BBJ, URLError
from string import punctuation from string import punctuation
from datetime import datetime from datetime import datetime
@ -62,6 +61,86 @@ welcome = """>>> Welcome to Bulletin Butter & Jelly! ------------------@
@_________________________________________________________@ @_________________________________________________________@
""" """
format_help = [
"BBJ supports **bolding**, __underlining__, and [rainbow: coloring] text "
"using markdown-style symbols as well as tag-like expressions. Markdown "
"is **NOT** fully implemented, but several of the more obvious concepts "
"have been brought over. Additionally, we have chan-style greentext and "
"numeric post referencing, ala >>3 for the third reply.",
"[red: Whitespace]",
"When you're composing, it is desirable to introduce linebreaks into the "
"body to keep it from overflowing the screen. However, you __dont__ want "
"that same spacing to bleed over to other people's screens, because clients "
"will wrap the text themselves.",
"Single line breaks in the body join into eachother to form sentences, "
"putting a space where the break was. This works like html. When you want "
"to split it off into a paragraph, **use two line breaks.**",
"[red: Colors, Bold, Underline & Expressions]",
"You can use [rainbow: rainbow], [red: red], [yellow: yellow], [green: green], "
"[blue: blue], [cyan: cyan], [magenta: and magenta], **bold**, and __underline__ "
"inside of your posts. **bold\nworks like this**, __and\nunderlines like this__. "
"The symbolic, markdown form of these directives does NOT allow escaping, and "
"can only apply to up to 20 characters on the same line. They are best used on short "
"phrases. However, you can use a different syntax for it, which is also required to use "
"colors: these expressions \[bold: look like this] and are much more reliable. "
"The colon and the space following it are important. When you use these "
"expressions, the __first__ space is not part of the content, but any characters, "
"including spaces, that follow it are included in the body. The formatting will "
"apply until the closing ]. You can escape such an expression \\[cyan: like this]"
"and can also \\[blue: escape \\] other closing brackets] inside of it. Only "
"closing brackets need to be escaped within an expression. Any backslashes used "
"for escaping will not show in the body unless you use two slashes.",
"This peculiar syntax elimiates false positives. You never have to escape [normal] "
"brackets when using the board. Only expressions with **valid and defined** directives "
"will be affected. [so: this is totally valid and requires no escapes] because 'so' is "
"not a directive. [red this will pass too] because the colon is missing.",
"The following directives may be used in this form: red, yellow, green, blue, cyan, "
"magenta, bold, underline, and rainbow. Nesting expressions into eachother will "
"override the parent directives until it closes. Thus, nesting is valid but doesn't produce "
"layered results.",
"[red: Quotes & Greentext]",
"You can refer to a post number using two angle brackets pointing into a number. >>432 "
"like this. You can color a whole line green by proceeding it with a '>'. Note that "
"this violates the sentence structure outlined in the **Whitespace** section above, "
"so you may introduce >greentext without splicing into seperate paragraphs. The '>' "
"must be the first character on the line with no whitespace before it.\n>it looks like this\n"
"and the paragraph doesnt have to break on either side.",
"When using numeric quotes, they are highlighted and the author's name will show "
"next to them in the thread. You can press enter when focused on a message to view "
"the parent posts. You may insert these directives manually or use the <Reply> function "
"on post menus.",
"Quoting directives cannot be escaped."
]
general_help = [
("bold", "use q or escape to close dialogs and menus (including this one)\n\n"),
"You may use the arrow keys, or use jk/np/Control-n|p to move up and down by "
"an element. If an element is overflowing the screen, it will scroll only one line. "
"To make scrolling faster, hold shift when using a control: it will repeat 5 times.\n\n"
"To go back and forth between threads, you may also use the left/right arrow keys, "
"or h/l to do it vi-style.\n\n"
"In the thread index and any open thread, the b and t keys may be used to go to "
"very top or bottom.\n\n"
"Aside from those, primary controls are shown on the very bottom of the screen "
"in the footer line, or may be placed in window titles for other actions like "
"dialogs or composers."
]
colornames = [ colornames = [
"none", "red", "yellow", "green", "blue", "none", "red", "yellow", "green", "blue",
"cyan", "magenta" "cyan", "magenta"
@ -87,7 +166,7 @@ default_prefs = {
class App(object): class App(object):
def __init__(self): def __init__(self):
self.bars = { self.bars = {
"index": "[Arrows|JK|NP]Navigate [RET]Open [C]ompose [R]efresh [O]ptions [?]Help [Q]uit", "index": "[JKNP]Navigate [RET]Open [C]ompose [R]efresh [O]ptions [?]Help [Q]uit",
"thread": "[C]ompose [RET]Interact [Q]Back [R]efresh [B/T]End [?]Help/More" "thread": "[C]ompose [RET]Interact [Q]Back [R]efresh [B/T]End [?]Help/More"
} }
@ -222,9 +301,8 @@ class App(object):
focus = "[focused on thread]" focus = "[focused on thread]"
attr = ("dim", "bar") attr = ("dim", "bar")
control = "[save/quit to send]" if self.prefs["editor"] else "[F3]Send"
self.loop.widget.footer[0].set_text( self.loop.widget.footer[0].set_text(
"[F1]Abort [F2]Swap %s %s" % (control, focus)) "[F1]Abort [F2]Swap [F3]Formatting Help [save/quit to send] " + focus)
# this hideous and awful sinful horrid unspeakable shithack changes # this hideous and awful sinful horrid unspeakable shithack changes
# the color of the help/title lines and editor border to reflect which # the color of the help/title lines and editor border to reflect which
@ -339,7 +417,7 @@ class App(object):
init_body=message["body"], init_body=message["body"],
post_id=post_id, post_id=post_id,
thread_id=thread_id), thread_id=thread_id),
title="[F1]Abort (save/quit to commit)", title="[F1]Abort [F3]Formatting Help (save/quit to commit)",
**frame_theme()), **frame_theme()),
self.loop.widget, self.loop.widget,
align="center", align="center",
@ -382,7 +460,7 @@ class App(object):
align=("relative", 50), align=("relative", 50),
valign=("relative", 50), valign=("relative", 50),
width=30, width=30,
height=len(buttons) + 3 height=len(buttons) + 2
) )
@ -399,7 +477,7 @@ class App(object):
return [value_type(q) for q in quotes] return [value_type(q) for q in quotes]
def make_message_body(self, message): def make_message_body(self, message, no_action=False):
""" """
Returns the widgets that comprise a message in a thread, including the Returns the widgets that comprise a message in a thread, including the
text body, author info and the action button text body, author info and the action button
@ -408,11 +486,12 @@ class App(object):
if message["edited"]: if message["edited"]:
info += " [edited]" info += " [edited]"
callback = self.on_post if not no_action else ignore
name = urwid.Text("~{}".format(self.usermap[message["author"]]["user_name"])) name = urwid.Text("~{}".format(self.usermap[message["author"]]["user_name"]))
post = str(message["post_id"]) post = str(message["post_id"])
head = urwid.Columns([ head = urwid.Columns([
(2 + len(post), urwid.AttrMap( (2 + len(post), urwid.AttrMap(
cute_button(">" + post, self.on_post, message), "button", "hover")), cute_button(">" + post, callback, message), "button", "hover")),
(len(name._text) + 1, urwid.AttrMap( (len(name._text) + 1, urwid.AttrMap(
name, str(self.usermap[message["author"]]["color"]))), name, str(self.usermap[message["author"]]["color"]))),
urwid.AttrMap(urwid.Text(info), "dim") urwid.AttrMap(urwid.Text(info), "dim")
@ -508,7 +587,7 @@ class App(object):
self.set_bars() self.set_bars()
def refresh(self, bottom=False): def refresh(self, bottom=True):
self.remove_overlays() self.remove_overlays()
if self.mode == "index": if self.mode == "index":
return self.index() return self.index()
@ -578,6 +657,60 @@ class App(object):
self.options_menu() self.options_menu()
def general_help(self):
"""
Show a general help dialog. In all honestly, its not
very useful and will only help people who have never
really used terminal software before =)
"""
widget = OptionsMenu(
urwid.ListBox(
urwid.SimpleFocusListWalker([
urwid_rainbows(
"This is BBJ, a client/server textboard made for tilde.town!",
True),
urwid.Text(("dim", "...by ~desvox")),
urwid.Divider("-"),
urwid.Button("Post Formatting Help", self.formatting_help),
urwid.Divider("-"),
urwid.Text(general_help)
])),
title="?????",
**frame_theme()
)
app.loop.widget = urwid.Overlay(
widget, app.loop.widget,
align=("relative", 50),
valign=("relative", 50),
width=30,
height=("relative", 60)
)
def formatting_help(self, *_):
"""
Pops a help window with formatting directives.
"""
message = network.fake_message("\n\n".join(format_help))
widget = OptionsMenu(
urwid.ListBox(
urwid.SimpleFocusListWalker([
*app.make_message_body(message, True)
])),
title="Formatting Help",
**frame_theme()
)
app.loop.widget = urwid.Overlay(
widget, app.loop.widget,
align=("relative", 50),
valign=("relative", 50),
width=("relative", 98),
height=("relative", 60)
)
def set_color(self, button, value, color): def set_color(self, button, value, color):
if value == False: if value == False:
return return
@ -854,7 +987,7 @@ class App(object):
if self.mode == "index": if self.mode == "index":
self.set_header('Composing "{}"', title) self.set_header('Composing "{}"', title)
self.set_footer("[F1]Abort [Save and quit to submit your thread]") self.set_footer("[F1]Abort [F3]Formatting Help [Save and quit to submit your thread]")
self.loop.widget = urwid.Overlay( self.loop.widget = urwid.Overlay(
urwid.LineBox( urwid.LineBox(
ExternalEditor("thread_create", title=title), ExternalEditor("thread_create", title=title),
@ -1049,15 +1182,17 @@ class ExternalEditor(urwid.Terminal):
else: else:
return app.temp_footer_message("EMPTY POST DISCARDED") return app.temp_footer_message("EMPTY POST DISCARDED")
elif key not in ["f1", "f2"]: elif key not in ["f1", "f2", "f3"]:
return super(ExternalEditor, self).keypress(size, key) return super(ExternalEditor, self).keypress(size, key)
elif key == "f1": elif key == "f1":
self.terminate() self.terminate()
app.close_editor() app.close_editor()
app.refresh() app.refresh()
# key == "f2" elif key == "f2":
app.switch_editor() app.switch_editor()
elif key == "f3":
app.formatting_help()
def __del__(self): def __del__(self):
@ -1077,6 +1212,12 @@ class OptionsMenu(urwid.LineBox):
# try to let the base class handle the key, if not, we'll take over # try to let the base class handle the key, if not, we'll take over
elif not super(OptionsMenu, self).keypress(size, key): elif not super(OptionsMenu, self).keypress(size, key):
return return
elif key in ["shift down", "J", "N"]:
for x in range(5):
self.keypress(size, "down")
elif key in ["shift up", "K", "P"]:
for x in range(5):
self.keypress(size, "up")
elif key.lower() == "q": elif key.lower() == "q":
app.loop.widget = app.loop.widget[0] app.loop.widget = app.loop.widget[0]
elif key in ["ctrl n", "j", "n"]: elif key in ["ctrl n", "j", "n"]:
@ -1102,11 +1243,11 @@ class ActionBox(urwid.ListBox):
elif key in ["k", "p", "ctrl p"]: elif key in ["k", "p", "ctrl p"]:
self._keypress_up(size) self._keypress_up(size)
elif key in ["J", "N"]: elif key in ["shift down", "J", "N"]:
for x in range(5): for x in range(5):
self._keypress_down(size) self._keypress_down(size)
elif key in ["K", "P"]: elif key in ["shift up", "K", "P"]:
for x in range(5): for x in range(5):
self._keypress_up(size) self._keypress_up(size)
@ -1131,6 +1272,9 @@ class ActionBox(urwid.ListBox):
elif key == "o": elif key == "o":
app.options_menu() app.options_menu()
elif key == "?":
app.general_help()
elif key.lower() == "q": elif key.lower() == "q":
app.back() app.back()
@ -1168,12 +1312,13 @@ def cute_button(label, callback=None, data=None):
return button return button
def urwid_rainbows(string): def urwid_rainbows(string, bold=False):
""" """
Same as below, but instead of printing rainbow text, returns Same as below, but instead of printing rainbow text, returns
a markup list suitable for urwid's Text contructor. a markup list suitable for urwid's Text contructor.
""" """
colors = [str(x) for x in range(1, 7)] colors = [str(x) for x in range(1, 7)]
if bold: colors = [(c + "0") for c in colors]
return urwid.Text([(choice(colors), char) for char in string]) return urwid.Text([(choice(colors), char) for char in string])
@ -1345,6 +1490,13 @@ def bbjrc(mode, **params):
return values return values
def ignore(*_, **__):
"""
The blackness of my soul.
"""
pass
def main(): def main():
run("clear", shell=True) run("clear", shell=True)

View File

@ -238,7 +238,9 @@ class API(object):
def thread_load(self, args, database, user, **kwargs): def thread_load(self, args, database, user, **kwargs):
""" """
Returns the thread object with all of its messages loaded. Returns the thread object with all of its messages loaded.
Requires the argument `thread_id` Requires the argument `thread_id`. `format` may also be
specified as a formatter to run the messages through.
Currently only "sequential" is supported.
""" """
validate(args, ["thread_id"]) validate(args, ["thread_id"])
thread = db.thread_get(database, args["thread_id"]) thread = db.thread_get(database, args["thread_id"])
@ -300,6 +302,23 @@ class API(object):
database, user["user_id"], args["thread_id"], args["post_id"]) database, user["user_id"], args["thread_id"], args["post_id"])
@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"]}]
if args["format"] == "sequential":
formatter = formatting.sequential_expressions
else:
raise BBJParameterError("invalid format directive.")
formatting.apply_formatting(message, formatter)
return message[0]["body"]
@api_method @api_method
def set_thread_pin(self, args, database, user, **kwargs): def set_thread_pin(self, args, database, user, **kwargs):
""" """

View File

@ -11,7 +11,7 @@ A B A N D O N ,:
/.' ' /.' '
This module includes a couple '/' This module includes a couple '/'
of custom (GROAN) formatting + of custom (GROAN) formatting +
specifications and parsers ' me irl specifications and parsers '
for them. Why did i do this? `. for them. Why did i do this? `.
I have no idea! .-"- I have no idea! .-"-
( | ( |
@ -70,7 +70,7 @@ colors = [
] ]
markup = [ markup = [
"bold", "italic", "underline", "linequote", "quote", "rainbow" "bold", "underline", "linequote", "quote", "rainbow"
] ]
# PS: regex parsing is no longer used for these, preserving anyways # PS: regex parsing is no longer used for these, preserving anyways
@ -82,6 +82,8 @@ markup = [
# quotes being references to other post_ids, like >>34 or >>0 for OP # quotes being references to other post_ids, like >>34 or >>0 for OP
quotes = re.compile(">>([0-9]+)") quotes = re.compile(">>([0-9]+)")
bold = re.compile(r"\*{2}(.{1,20})\*{2}")
underline = re.compile(r"__(.{1,20})__")
def parse_segments(text, sanitize_linequotes=True): def parse_segments(text, sanitize_linequotes=True):
@ -98,6 +100,8 @@ def parse_segments(text, sanitize_linequotes=True):
if not segment: if not segment:
continue continue
segment = quotes.sub(lambda m: "[quote: %s]" % m.group(1), segment) segment = quotes.sub(lambda m: "[quote: %s]" % m.group(1), segment)
segment = bold.sub(lambda m: "[bold: %s]" % m.group(1), segment)
segment = underline.sub(lambda m: "[underline: %s]" % m.group(1), segment)
if segment.startswith(">"): if segment.startswith(">"):
if sanitize_linequotes: if sanitize_linequotes:
inner = segment.replace("]", "\\]") inner = segment.replace("]", "\\]")