From 19af814a933fa561fd2caf8da541d75b6f592c33 Mon Sep 17 00:00:00 2001 From: Blake DeMarcy Date: Wed, 5 Apr 2017 13:09:38 -0500 Subject: [PATCH] begin urwid client --- clients/network_client.py | 55 ++++++-- clients/urwid/main.py | 248 +++++++++++++++++++++++++++++++++ clients/urwid/network.py | 1 + clients/urwid/not now but soon | 3 - 4 files changed, 292 insertions(+), 15 deletions(-) create mode 100644 clients/urwid/main.py create mode 120000 clients/urwid/network.py delete mode 100644 clients/urwid/not now but soon diff --git a/clients/network_client.py b/clients/network_client.py index ec8e045..f578bc8 100644 --- a/clients/network_client.py +++ b/clients/network_client.py @@ -42,8 +42,7 @@ class BBJ: """ def __init__(self, host="127.0.0.1", port=8080): self.base = "http://{}:{}/api/%s".format(host, port) - self.user_name = None - self.user_auth = None + self.user_name = self.user_auth = None self.send_auth = True @@ -101,7 +100,8 @@ class BBJ: Uses the server's db_sanity_check method to verify the validty of value by key. If it is invalid, kwarg exception (default AssertionError) is raised with the exception containing the - attribute .description as the server's reason. + attribute .description as the server's reason. Exception can + be a False value to just rturn boolean False. """ response = self( "db_sanity_check", @@ -111,6 +111,8 @@ class BBJ: ) if not response["data"]["bool"]: + if not exception: + return False description = response["data"]["description"] error = exception(description) error.description = description @@ -148,8 +150,7 @@ class BBJ: user_auth = sha256(bytes(user_auth, "utf8")).hexdigest() if check_validity and not self.validate_credentials(user_name, user_auth): - self.user_auth = None - self.user_name = None + self.user_auth = self.user_name = None raise ConnectionRefusedError("Auth and User do not match") self.user_auth = user_auth @@ -180,22 +181,44 @@ class BBJ: Returns True or False whether user_name is registered into the system. """ - return self( + response = self( "user_is_registered", no_auth=True, target_user=user_name - )["data"] + ) + + return response["data"] - def user_register(self, user_name, user_auth, hash_auth=True): + def user_register(self, user_name, user_auth, hash_auth=True, set_as_user=True): """ Register user_name into the system with user_auth. Unless hash_auth is set to false, user_auth should be a password string. - """ - pass - # return self( - # ) + When set_as_user is True, the newly registered user is internalizedn + and subsequent uses of the object will be authorized for them. + """ + if hash_auth: + user_auth = sha256(bytes(user_auth, "utf8")).hexdigest() + + response = self( + "user_register", + no_auth=True, + user_name=user_name, + auth_hash=user_auth + )["data"] + + assert all([ + user_auth == response["auth_hash"], + user_name == response["user_name"] + ]) + + if set_as_user: + self.user_name = user_name + self.user_auth = user_auth + + return response + def thread_index(self): """ @@ -204,3 +227,11 @@ class BBJ: """ response = self("thread_index") return response["data"], response["usermap"] + + + def thread_load(self, thread_id): + """ + Returns a tuple where [0] is a thread object and [1] is a usermap object. + """ + response = self("thread_load", thread_id=thread_id) + return response["data"], response["usermap"] diff --git a/clients/urwid/main.py b/clients/urwid/main.py new file mode 100644 index 0000000..983bb78 --- /dev/null +++ b/clients/urwid/main.py @@ -0,0 +1,248 @@ +from time import sleep, localtime +from string import punctuation +from subprocess import run +from random import choice +from network import BBJ +import urwid + +network = BBJ(host="127.0.0.1", port=8080) + +obnoxious_logo = """ + OwO 8 888888888o 8 888888888o 8 8888 1337 + % 8 8888 `88. 8 8888 `88. ! 8 8888 >><> + !! 8 8888 `88 8 8888 `88 * 8 8888 + $ 8 8888 ,88 8 8888 ,88 8 8888 <><><><> + <3 8 8888. ,88' 8 8888. ,88' ! 8 8888 ^ >| + ^ 8 8888888888 8 8888888888 8 8888 ----|| + ( 8 8888 `88. 8 8888 `88. 88. 8 8888 | + 8 8888 88 8 8888 88 `88. | 8 888' !??! + g ? 8 8888. ,88' 8 8888. ,88' `88o. .8 88' ----_ + 8 888888888P 8 888888888P `Y888888 ' | +""" + +welcome = """ +>>> Welcome to Bulletin Butter & Jelly! ---------------------------------------@ +| BBJ is a persistent, chronologically ordered discussion board for tilde.town.| +| You may log in, register as a new user, or participate anonymously. | +| \033[1;31mTo go anon, just press enter. Otherwise, give me a name (registered or not)\033[0m | +@______________________________________________________________________________@ +""" + +colors = [ + "\033[1;31m", "\033[1;33m", "\033[1;33m", + "\033[1;32m", "\033[1;34m", "\033[1;35m" +] + + +class App: + def __init__(self): + colors = [ + ("bar", "light magenta", "default", "underline"), + ("button", "light red", "default") + ] + self.loop = urwid.MainLoop(urwid.Frame( + urwid.LineBox(ActionBox(urwid.SimpleFocusListWalker([]))), + ), colors) + self.date_format = "{1}/{2}/{0}" + self.index() + + + def set_header(self, text, *format_specs): + self.loop.widget.header = urwid.AttrMap(urwid.Text( + ("%s@bbj | " % network.user_name) + + text.format(*format_specs) + ), "bar") + + + def set_footer(self, *controls): + text = str() + for control in controls: + text += "[{}]{} ".format(control[0], control[1:]) + self.loop.widget.footer = urwid.AttrMap(urwid.Text(text), "bar") + + + def readable_delta(self, created, modified): + delta = modified - created + minutes = delta // 60 + if not minutes: + return "less than a minute ago" + elif minutes < 60: + return "%d minutes ago" % minutes + hours = delta // 60 + if hours == 1: + return "about an hour ago" + elif hours < 48: + return "%d hours ago" % hours + return self.date_format.format(*localtime(modified)) + + + def index(self): + threads, usermap = network.thread_index() + self.set_header("{} threads", len(threads)) + self.set_footer("Compose") + walker = self.loop.widget.body.base_widget.body + for thread in threads: + button = urwid.Button("", self.thread_load, thread["thread_id"]) + super(urwid.Button, button).__init__( + urwid.SelectableIcon(">>")) + title = urwid.Text(thread["title"]) + + last_mod = thread["last_mod"] + created = thread["created"] + infoline = "by ~{} @ {} | last active {}".format( + usermap[thread["author"]]["user_name"], + self.date_format.format(*localtime(created)), + self.readable_delta(created, last_mod) + ) + + walker.append(urwid.Columns([(3, urwid.AttrMap(button, "button")), title])) + walker.append(urwid.Text(infoline)) + walker.append(urwid.Divider("-")) + + + def thread_load(self, button, thread_id): + thread, usermap = network.thread_load(thread_id) + walker = self.loop.widget.body.base_widget.body + walker.clear() + self.set_header("~{}: {}", + usermap[thread["author"]]["user_name"], thread["title"]) + for message in thread["messages"]: + pass + + +class ActionBox(urwid.ListBox): + pass + + +def motherfucking_rainbows(string, inputmode=False, end="\n"): + """ + I cANtT FeELLE MyYE FACECsEE ANYrrMOROeeee!! + """ + for character in string: + print(choice(colors) + character, end="") + print('\033[0m', end="") + if inputmode: + return input("") + return print(end, end="") + + +def paren_prompt(text, positive=True, choices=[]): + """ + input(), but riced the fuck out. Changes color depending on + the value of positive (blue/green for good stuff, red/yellow + for bad stuff like invalid input), and has a multiple choice + system capable of rejecting unavailable choices and highlighting + their first characters. + """ + end = text[-1] + if end != "?" and end in punctuation: + text = text[0:-1] + + mood = ("\033[1;36m", "\033[1;32m") if positive \ + else ("\033[1;31m", "\033[1;33m") + + if choices: + prompt = "%s{" % mood[0] + for choice in choices: + prompt += "{0}[{1}{0}]{2}{3} ".format( + "\033[1;35m", choice[0], mood[1], choice[1:]) + formatted_choices = prompt[:-1] + ("%s}" % mood[0]) + else: + formatted_choices = "" + + try: + response = input("{0}({1}{2}{0}){3}> \033[0m".format( + *mood, text, formatted_choices)) + if not choices: + return response + elif response == "": + response = " " + char = response.lower()[0] + if char in [c[0] for c in choices]: + return char + return paren_prompt("Invalid choice", False, choices) + + except EOFError: + print("") + return "" + + except KeyboardInterrupt: + exit("\nNevermind then!") + + +def sane_value(key, prompt, positive=True, return_empty=False): + response = paren_prompt(prompt, positive) + if return_empty and response == "": + return response + try: network.validate(key, response) + except AssertionError as e: + return sane_value(key, e.description, False) + return response + + +def log_in(): + """ + Handles login or registration using the oldschool input() + method. The user is run through this before starting the + curses app. + """ + name = sane_value("user_name", "Username", return_empty=True) + if name == "": + motherfucking_rainbows("~~W3 4R3 4n0nYm0u5~~") + else: + # ConnectionRefusedError means registered but needs a + # password, ValueError means we need to register the user. + try: + network.set_credentials(name, "") + # make it easy for people who use an empty password =) + motherfucking_rainbows("~~logged in as {}~~".format(network.user_name)) + + except ConnectionRefusedError: + def login_loop(prompt, positive): + try: + password = paren_prompt(prompt, positive) + network.set_credentials(name, password) + except ConnectionRefusedError: + login_loop("// R E J E C T E D //.", False) + + login_loop("Enter your password", True) + motherfucking_rainbows("~~logged in as {}~~".format(network.user_name)) + + except ValueError: + motherfucking_rainbows("Nice to meet'cha, %s!" % name) + response = paren_prompt( + "Register as %s?" % name, + choices=["yes!", "change name"] + ) + + if response == "c": + name = sane_value("user_name", "Pick a new name") + + def password_loop(prompt, positive=True): + response1 = paren_prompt(prompt, positive) + if response1 == "": + confprompt = "Confirm empty password" + else: + confprompt = "Confirm it" + response2 = paren_prompt(confprompt) + if response1 != response2: + return password_loop("Those didnt match. Try again", False) + return response1 + + password = password_loop("Enter a password. It can be empty if you want") + network.user_register(name, password) + motherfucking_rainbows("~~welcome to the party, %s!~~" % network.user_name) + + +def main(): + run("clear", shell=True) + motherfucking_rainbows(obnoxious_logo) + print(welcome) + log_in() + # sleep(1) # let that confirmation message shine + +if __name__ == "__main__": + global app + main() + app = App() + app.loop.run() diff --git a/clients/urwid/network.py b/clients/urwid/network.py new file mode 120000 index 0000000..b69adfd --- /dev/null +++ b/clients/urwid/network.py @@ -0,0 +1 @@ +../network_client.py \ No newline at end of file diff --git a/clients/urwid/not now but soon b/clients/urwid/not now but soon deleted file mode 100644 index 0e55cf3..0000000 --- a/clients/urwid/not now but soon +++ /dev/null @@ -1,3 +0,0 @@ -sorry - -emacs is cooler anyway 8) \ No newline at end of file