Compare commits

...

16 Commits

Author SHA1 Message Date
magical 804438f045 add bundle.py to create single-file bbj client
python is able to import modules from a zip file. if the zip file
contains __main__.py it will even run it as a script! this lets us bundle
bbj's frontend together with its sole external dependency (urwid) to
create a single executable file that'll run anywhere with python
installed, no virtualenv needed.

the only downside is that python can't import shared objects (.so) from a
zip file, so urwid can't use its C-accelerated str_util module and has
to fall back to the python version. which is slower, probably.
2022-08-10 07:09:06 +00:00
magical ab1550c09c redo mark function 2022-08-10 03:44:15 +00:00
magical 315a249ef6 check for empty post before asking for confirmation
before this change, BBJ would give the big "You are posting
anonymously!" warning even if the post body was empty (say, because you
accidentally started a post and closed the editor immediately). only
after confirming that you _really_ wanted to post anonymously would it
notice that, hey, the post body is empty and discard the post.

it makes more sense to do the checks the other way around.
2022-08-08 01:51:37 +00:00
magical 92bdd7b660 improve wipe_screen 2022-08-07 01:39:18 +00:00
magical 77d38011b5 quote file path in shell command 2022-08-07 01:25:14 +00:00
magical 4364c16625 set env in subprocess a little more cleanly 2022-08-07 01:25:14 +00:00
magical deb8d3ba4b remove unnecessary shell=True's 2022-08-07 01:25:14 +00:00
magical f63233dbb7 don't use network.request 2022-08-07 01:25:14 +00:00
magical ab42b4a27d use network.request 2022-08-07 01:25:14 +00:00
magical 8ae384b1cd make sane_value validation a little more general 2022-08-06 06:42:20 +00:00
magical 5e7168c848 rewrite some tail call loops as normal loops 2022-08-06 06:20:16 +00:00
magical 2e54939345 remove global network value 2022-08-06 06:01:25 +00:00
magical ceb4937c60 remove relog parameter 2022-08-06 05:40:25 +00:00
magical 3e4f3de2a8 pass the initial username and password to log_in explicitly 2022-08-06 05:20:39 +00:00
magical b1d1de659d import exit
The builtin exit function is added by the site package "for the
interactive interpreter" and "should not be used in programs", according
to the manual.

We want sys.exit instead. (I didn't know you could pass it a string
value, but it turns out you can.)
2022-08-06 05:10:14 +00:00
magical df9d419919 move some global code into main 2022-08-06 05:09:57 +00:00
2 changed files with 229 additions and 163 deletions

32
bundle.py 100644
View File

@ -0,0 +1,32 @@
import zipfile
import glob
import os
# TODO: should we include .pyc files?
# TODO: add urwid source into the repo somewhere
files = {
'__main__.py': 'clients/urwid/main.py',
'network.py': 'clients/network_client.py',
'urwid': 'env/lib/python3.8/site-packages/urwid/*.py',
}
with open('bbj_demo', 'wb') as f:
f.write(b"#!/usr/bin/env python3\n")
with zipfile.ZipFile(f, 'w', compression=zipfile.ZIP_DEFLATED) as z:
z.comment = b'BBJ'
for name, source in files.items():
if '*' in source:
dirname = name
for path in sorted(glob.glob(source)):
name = dirname + '/' + os.path.basename(path)
z.write(path, name)
else:
z.write(source, name)
try:
mask = os.umask(0)
os.umask(mask)
except OSError:
mask = 0
os.chmod(z.filename, 0o777&~mask)

View File

@ -29,27 +29,18 @@ from getpass import getpass
from subprocess import call from subprocess import call
from random import choice from random import choice
from code import interact from code import interact
from sys import exit
import rlcompleter import rlcompleter
import readline import readline
import tempfile import tempfile
import shlex
import urwid import urwid
import json import json
import os import os
import re import re
# XxX_N0_4rgP4rs3_XxX ###yoloswag help_text = """\
def get_arg(key, default=None, get_value=True): BBJ Urwid Client
try:
spec = argv.index("--" + key)
value = argv[spec + 1] if get_value else True
except ValueError: # --key not specified
value = default
except IndexError: # flag given but no value
exit("invalid format for --" + key)
return value
if get_arg("help", False, False):
print("""BBJ Urwid Client
Available options: Available options:
--help: this message --help: this message
--https: enable use of https, requires host support --https: enable use of https, requires host support
@ -62,16 +53,7 @@ Available environment variables:
BBJ_PASSWORD: set your password to log in automatically. BBJ_PASSWORD: set your password to log in automatically.
if the password is wrong, will prompt you as normal. if the password is wrong, will prompt you as normal.
Please note that these environment variables need to be exported, and are Please note that these environment variables need to be exported, and are
visible to all other programs run from your shell.""") visible to all other programs run from your shell."""
exit()
try:
network = BBJ(get_arg("host", "127.0.0.1"),
get_arg("port", 7099),
get_arg("https", False, False))
except URLError as e:
# print the connection error in red
exit("\033[0;31m%s\033[0m" % repr(e))
obnoxious_logo = """ obnoxious_logo = """
% _ * ! * % _ * ! *
@ -328,7 +310,8 @@ markpath = os.path.join(os.getenv("HOME"), ".bbjmarks")
pinpath = os.path.join(os.getenv("HOME"), ".bbjpins") pinpath = os.path.join(os.getenv("HOME"), ".bbjpins")
class App(object): class App(object):
def __init__(self): def __init__(self, network):
self.network = network
self.prefs = bbjrc("load") self.prefs = bbjrc("load")
self.client_pinned_threads = load_client_pins() self.client_pinned_threads = load_client_pins()
self.usermap = {} self.usermap = {}
@ -387,7 +370,7 @@ class App(object):
bar formatting to it. bar formatting to it.
""" """
header = ("{}@bbj | " + text).format( header = ("{}@bbj | " + text).format(
(network.user_name or "anonymous"), (self.network.user_name or "anonymous"),
*format_specs *format_specs
) )
self.loop.widget.header = urwid.AttrMap(urwid.Text(header), "bar") self.loop.widget.header = urwid.AttrMap(urwid.Text(header), "bar")
@ -593,7 +576,7 @@ class App(object):
# first we need to get the server's version of the message # first we need to get the server's version of the message
# instead of our formatted one # instead of our formatted one
try: try:
message = network.edit_query(thread_id, post_id) message = self.network.edit_query(thread_id, post_id)
except UserWarning as e: except UserWarning as e:
self.remove_overlays() self.remove_overlays()
return self.temp_footer_message(e.description) return self.temp_footer_message(e.description)
@ -616,7 +599,7 @@ class App(object):
urwid.Text(("bold", "Delete this %s?" % ("whole thread" if op else "post"))), urwid.Text(("bold", "Delete this %s?" % ("whole thread" if op else "post"))),
urwid.Divider(), urwid.Divider(),
cute_button(("10" , ">> Yes"), lambda _: [ cute_button(("10" , ">> Yes"), lambda _: [
network.message_delete(message["thread_id"], message["post_id"]), self.network.message_delete(message["thread_id"], message["post_id"]),
self.remove_overlays(), self.remove_overlays(),
self.index() if op else self.refresh() self.index() if op else self.refresh()
]), ]),
@ -638,7 +621,7 @@ class App(object):
def toggle_formatting(self, button, message): def toggle_formatting(self, button, message):
self.remove_overlays() self.remove_overlays()
raw = not message["send_raw"] raw = not message["send_raw"]
network.set_post_raw(message["thread_id"], message["post_id"], raw) self.network.set_post_raw(message["thread_id"], message["post_id"], raw)
return self.refresh() return self.refresh()
@ -655,7 +638,7 @@ class App(object):
"View %sQuote" % ("a " if len(quotes) != 1 else ""), "View %sQuote" % ("a " if len(quotes) != 1 else ""),
self.quote_view_menu, quotes)) self.quote_view_menu, quotes))
if network.can_edit(message["thread_id"], message["post_id"]) \ if self.network.can_edit(message["thread_id"], message["post_id"]) \
and not self.window_split: and not self.window_split:
if message["post_id"] == 0: if message["post_id"] == 0:
@ -668,7 +651,7 @@ class App(object):
"Enable Formatting" if raw else "Disable Formatting", "Enable Formatting" if raw else "Disable Formatting",
self.toggle_formatting, message)) self.toggle_formatting, message))
buttons.insert(0, urwid.Button("Edit Post", self.edit_post, message)) buttons.insert(0, urwid.Button("Edit Post", self.edit_post, message))
if network.user["is_admin"]: if self.network.user["is_admin"]:
buttons.insert(0, urwid.Text(("20", "Reminder: You're an admin!"))) buttons.insert(0, urwid.Text(("20", "Reminder: You're an admin!")))
if not buttons: if not buttons:
@ -810,7 +793,7 @@ class App(object):
""" """
if self.mode == "thread": if self.mode == "thread":
# mark the current position in this thread before going back to the index # mark the current position in this thread before going back to the index
mark() self.mark()
self.body.attr_map = {None: "default"} self.body.attr_map = {None: "default"}
self.mode = "index" self.mode = "index"
@ -821,7 +804,7 @@ class App(object):
# narrowed selection of content, so we dont want to resume last_index_pos # narrowed selection of content, so we dont want to resume last_index_pos
self.last_index_pos = False self.last_index_pos = False
else: else:
threads, usermap = network.thread_index() threads, usermap = self.network.thread_index()
self.usermap.update(usermap) self.usermap.update(usermap)
self.walker.clear() self.walker.clear()
@ -867,7 +850,7 @@ class App(object):
self.body.attr_map = {None: "default"} self.body.attr_map = {None: "default"}
self.mode = "thread" self.mode = "thread"
thread, usermap = network.thread_load(thread_id, format="sequential") thread, usermap = self.network.thread_load(thread_id, format="sequential")
self.usermap.update(usermap) self.usermap.update(usermap)
self.thread = thread self.thread = thread
self.match_data["matches"].clear() self.match_data["matches"].clear()
@ -876,7 +859,7 @@ class App(object):
self.walker += self.make_message_body(message) self.walker += self.make_message_body(message)
self.set_default_header() self.set_default_header()
self.set_default_footer() self.set_default_footer()
self.goto_post(mark(thread_id)) self.goto_mark(thread_id)
def toggle_client_pin(self): def toggle_client_pin(self):
@ -888,16 +871,16 @@ class App(object):
def toggle_server_pin(self): def toggle_server_pin(self):
if self.mode != "index" or not network.user["is_admin"]: if self.mode != "index" or not self.network.user["is_admin"]:
return return
thread = self.walker.get_focus()[0].thread thread = self.walker.get_focus()[0].thread
network.thread_set_pin(thread["thread_id"], not thread["pinned"]) self.network.thread_set_pin(thread["thread_id"], not thread["pinned"])
self.index() self.index()
def search_index_callback(self, query): def search_index_callback(self, query):
simple_query = query.lower().strip() simple_query = query.lower().strip()
threads, usermap = network.thread_index() threads, usermap = self.network.thread_index()
self.usermap.update(usermap) self.usermap.update(usermap)
results = [ results = [
thread for thread in threads thread for thread in threads
@ -1000,10 +983,10 @@ class App(object):
self.last_index_pos = self.get_focus_post(True).thread["thread_id"] self.last_index_pos = self.get_focus_post(True).thread["thread_id"]
self.index() self.index()
else: else:
mark() self.mark()
thread = self.thread["thread_id"] thread = self.thread["thread_id"]
self.thread_load(None, thread) self.thread_load(None, thread)
self.goto_post(mark(thread)) self.goto_mark(thread)
self.temp_footer_message("Refreshed content!", 1) self.temp_footer_message("Refreshed content!", 1)
@ -1038,10 +1021,31 @@ class App(object):
width=30, height=6) width=30, height=6)
else: else:
mark() self.mark()
self.index() self.index()
def mark(self, thread_id=None):
if self.mode != "thread":
return
if thread_id is None:
thread_id = self.thread['thread_id']
pos = self.get_focus_post()
mark(thread_id, pos, default=0)
return pos
def goto_mark(self, thread_id=None):
if self.mode != "thread":
return
if thread_id is None:
thread_id = self.thread['thread_id']
pos = mark(thread_id, default=0)
self.goto_post(pos)
def get_focus_post(self, return_widget=False): def get_focus_post(self, return_widget=False):
pos = self.box.get_focus_path()[0] pos = self.box.get_focus_path()[0]
if self.mode == "thread": if self.mode == "thread":
@ -1172,10 +1176,10 @@ class App(object):
""" """
self.loop.widget = self.loop.widget[0] self.loop.widget = self.loop.widget[0]
self.loop.stop() self.loop.stop()
call("clear", shell=True) call("clear")
print(welcome) print(welcome)
try: try:
log_in(relog=True) log_in(self.network)
except (KeyboardInterrupt, InterruptedError): except (KeyboardInterrupt, InterruptedError):
pass pass
self.loop.start() self.loop.start()
@ -1188,8 +1192,9 @@ class App(object):
Options menu callback to anonymize the user and Options menu callback to anonymize the user and
then redisplay the options menu. then redisplay the options menu.
""" """
network.user_name = network.user_auth = None self.network.user_name = None
network.user = network("get_me")["data"] self.network.user_auth = None
self.network.user = self.network.request("get_me")["data"]
self.loop.widget = self.loop.widget[0] self.loop.widget = self.loop.widget[0]
self.set_default_header() self.set_default_header()
self.options_menu() self.options_menu()
@ -1231,7 +1236,7 @@ class App(object):
""" """
# we can "recycle" the server's formatting abilities to # we can "recycle" the server's formatting abilities to
# use the same syntax for the help text itself # use the same syntax for the help text itself
message = network.fake_message( message = self.network.fake_message(
"\n\n".join(format_help), format="sequential") "\n\n".join(format_help), format="sequential")
widget = OptionsMenu( widget = OptionsMenu(
@ -1253,7 +1258,7 @@ class App(object):
def set_color(self, button, value, color): def set_color(self, button, value, color):
if value == False: if value == False:
return return
network.user_update(color=color) self.network.user_update(color=color)
def toggle_exit(self, button, value): def toggle_exit(self, button, value):
@ -1280,10 +1285,10 @@ class App(object):
def change_username(self, *_): def change_username(self, *_):
self.loop.stop() self.loop.stop()
call("clear", shell=True) call("clear")
try: try:
name = nameloop("Choose a new username", True) name = nameloop(self.network, "Choose a new username")
network.user_update(user_name=name) self.network.user_update(user_name=name)
motherfucking_rainbows("~~hello there %s~~" % name) motherfucking_rainbows("~~hello there %s~~" % name)
sleep(0.8) sleep(0.8)
self.loop.start() self.loop.start()
@ -1296,10 +1301,10 @@ class App(object):
def change_password(self, *_): def change_password(self, *_):
self.loop.stop() self.loop.stop()
call("clear", shell=True) call("clear")
try: try:
password = password_loop("Choose a new password. Can be empty", True) password = password_loop("Choose a new password. Can be empty")
network.user_update(auth_hash=network._hash(password)) self.network.user_update(auth_hash=self.network._hash(password))
motherfucking_rainbows("SET NEW PASSWORD") motherfucking_rainbows("SET NEW PASSWORD")
sleep(0.8) sleep(0.8)
self.loop.start() self.loop.start()
@ -1395,8 +1400,8 @@ class App(object):
editor_buttons = [] editor_buttons = []
edit_mode = [] edit_mode = []
if network.user_auth: if self.network.user_auth:
account_message = "Logged in as %s." % network.user_name account_message = "Logged in as %s." % self.network.user_name
account_stuff = [ account_stuff = [
urwid.Button("Relog", on_press=self.relog), urwid.Button("Relog", on_press=self.relog),
urwid.Button("Go anonymous", on_press=self.unlog), urwid.Button("Go anonymous", on_press=self.unlog),
@ -1413,7 +1418,7 @@ class App(object):
for index, color in enumerate(colornames): for index, color in enumerate(colornames):
urwid.RadioButton( urwid.RadioButton(
user_colors, color.title(), user_colors, color.title(),
network.user["color"] == index, self.network.user["color"] == index,
self.set_color, index) self.set_color, index)
for item in user_colors: for item in user_colors:
@ -1618,7 +1623,9 @@ class App(object):
descriptor, path = tempfile.mkstemp() descriptor, path = tempfile.mkstemp()
with open(path, "w") as _: with open(path, "w") as _:
_.write(init_body) _.write(init_body)
call("export LANG=en_US.UTF-8; %s %s" % (self.prefs["editor"], path), shell=True) env = os.environ.copy()
env['LANG'] = 'en_US.UTF-8'
call("%s %s" % (self.prefs["editor"], shlex.quote(path)), env=env, shell=True)
with open(path) as _: with open(path) as _:
body = _.read() body = _.read()
os.remove(path) os.remove(path)
@ -1635,7 +1642,7 @@ class App(object):
return self.footer_prompt("Title", self.compose) return self.footer_prompt("Title", self.compose)
elif title: elif title:
try: network.validate("title", title) try: self.network.validate("title", title)
except AssertionError as e: except AssertionError as e:
return self.footer_prompt( return self.footer_prompt(
"Title", self.compose, extra_text=e.description) "Title", self.compose, extra_text=e.description)
@ -1644,24 +1651,19 @@ class App(object):
body = self.overthrow_ext_edit(init_body) body = self.overthrow_ext_edit(init_body)
if not body or re.search("^>>[0-9]+$", body): if not body or re.search("^>>[0-9]+$", body):
return self.temp_footer_message("EMPTY POST DISCARDED") return self.temp_footer_message("EMPTY POST DISCARDED")
params = {"body": body}
if self.mode == "thread" and not edit: if self.mode == "thread" and not edit:
endpoint = "thread_reply" self.network.thread_reply(self.thread["thread_id"], body)
params.update({"thread_id": self.thread["thread_id"]})
elif edit: elif edit:
endpoint = "edit_post" self.network.edit_message(
params.update({ thread_id=self.thread["thread_id"],
"thread_id": self.thread["thread_id"], post_id=edit["post_id"],
"post_id": edit["post_id"] body=body)
})
else: else:
endpoint = "thread_create" self.network.thread_create(title, body)
params.update({"title": title})
network.request(endpoint, **params)
self.refresh() self.refresh()
if edit: if edit:
self.goto_post(edit["post_id"]) self.goto_post(edit["post_id"])
@ -1714,6 +1716,12 @@ class App(object):
self.window_split=True self.window_split=True
self.switch_editor() self.switch_editor()
def repaint_screen(self):
"""
Force urwid to repaint the whole screen.
"""
self.loop.screen.clear()
class MessageBody(urwid.Text): class MessageBody(urwid.Text):
""" """
@ -1770,7 +1778,7 @@ class MessageBody(urwid.Text):
if _c != 0: if _c != 0:
color = str(_c) color = str(_c)
if user != "anonymous" and user["user_name"] == network.user_name: if user != "anonymous" and user["user_name"] == app.network.user_name:
display = "[You]" display = "[You]"
# bold it # bold it
color += "0" color += "0"
@ -1978,7 +1986,7 @@ class ExternalEditor(urwid.Terminal):
# should use the options menu to switch to Overthrow mode. # should use the options menu to switch to Overthrow mode.
env.update({"LANG": "POSIX"}) env.update({"LANG": "POSIX"})
command = ["bash", "-c", "{} {}; echo Press any key to kill this window...".format( command = ["bash", "-c", "{} {}; echo Press any key to kill this window...".format(
app.prefs["editor"], self.path)] app.prefs["editor"], shlex.quote(self.path))]
super(ExternalEditor, self).__init__(command, env, app.loop, app.prefs["edit_escapes"]["abort"]) super(ExternalEditor, self).__init__(command, env, app.loop, app.prefs["edit_escapes"]["abort"])
urwid.connect_signal(self, "closed", self.exterminate) urwid.connect_signal(self, "closed", self.exterminate)
@ -1987,7 +1995,7 @@ class ExternalEditor(urwid.Terminal):
# app.loop.widget = app.loop.widget[0] # app.loop.widget = app.loop.widget[0]
# if not value: # if not value:
# app.loop.stop() # app.loop.stop()
# call("clear", shell=True) # call("clear")
# print(welcome) # print(welcome)
# log_in(True) # log_in(True)
# app.loop.start() # app.loop.start()
@ -1995,9 +2003,21 @@ class ExternalEditor(urwid.Terminal):
def exterminate(self, *_, anon_confirmed=False): def exterminate(self, *_, anon_confirmed=False):
# close the editor and grab the post body
app.close_editor()
with open(self.path) as _:
body = _.read().strip()
os.remove(self.path)
# make sure its not empty
if not body or re.search("^>>[0-9]+$", body):
app.temp_footer_message("EMPTY POST DISCARDED")
return
# are we anonymous? check if the user wants to log in first
if app.prefs["confirm_anon"] \ if app.prefs["confirm_anon"] \
and not anon_confirmed \ and not anon_confirmed \
and network.user["user_name"] == "anonymous": and app.network.user["user_name"] == "anonymous":
# TODO fixoverlay: urwid terminal widgets have been mucking # TODO fixoverlay: urwid terminal widgets have been mucking
# up overlay dialogs since the wee days of bbj, i really # up overlay dialogs since the wee days of bbj, i really
# need to find a real solution instead of dodging the issue # need to find a real solution instead of dodging the issue
@ -2024,7 +2044,7 @@ class ExternalEditor(urwid.Terminal):
# return # return
# else: # else:
app.loop.stop() app.loop.stop()
call("clear", shell=True) call("clear")
print(anon_warn) print(anon_warn)
choice = paren_prompt( choice = paren_prompt(
"Post anonymously?", default="yes", choices=["Yes", "no"] "Post anonymously?", default="yes", choices=["Yes", "no"]
@ -2033,27 +2053,20 @@ class ExternalEditor(urwid.Terminal):
log_in(True) log_in(True)
app.loop.start() app.loop.start()
app.close_editor() # ok; do the post
with open(self.path) as _: self.params.update({"body": body})
body = _.read().strip() app.network.request(self.endpoint, **self.params)
os.remove(self.path) if self.endpoint == "edit_post":
app.refresh()
app.goto_post(self.params["post_id"])
if body and not re.search("^>>[0-9]+$", body): elif app.mode == "thread":
self.params.update({"body": body}) app.refresh()
network.request(self.endpoint, **self.params) app.goto_post(app.thread["reply_count"])
if self.endpoint == "edit_post":
app.refresh()
app.goto_post(self.params["post_id"])
elif app.mode == "thread":
app.refresh()
app.goto_post(app.thread["reply_count"])
else:
app.last_pos = None
app.index()
else: else:
app.temp_footer_message("EMPTY POST DISCARDED") app.last_pos = None
app.index()
def keypress(self, size, key): def keypress(self, size, key):
@ -2071,7 +2084,7 @@ class ExternalEditor(urwid.Terminal):
if keyl == "ctrl l": if keyl == "ctrl l":
# always do this, and also pass it to the terminal # always do this, and also pass it to the terminal
wipe_screen() app.repaint_screen()
elif key == app.prefs["edit_escapes"]["abort"]: elif key == app.prefs["edit_escapes"]["abort"]:
self.terminate() self.terminate()
@ -2149,7 +2162,7 @@ class OptionsMenu(urwid.LineBox):
return self.keypress(size, "enter") return self.keypress(size, "enter")
elif keyl == "ctrl l": elif keyl == "ctrl l":
wipe_screen() app.repaint_screen()
def mouse_event(self, size, event, button, x, y, focus): def mouse_event(self, size, event, button, x, y, focus):
@ -2205,7 +2218,7 @@ class ActionBox(urwid.ListBox):
self.change_focus(size, 0) self.change_focus(size, 0)
elif key == "ctrl l": elif key == "ctrl l":
wipe_screen() app.repaint_screen()
elif keyl == "o": elif keyl == "o":
app.options_menu() app.options_menu()
@ -2252,13 +2265,13 @@ class ActionBox(urwid.ListBox):
elif key == "~": elif key == "~":
# sssssshhhhhhhh # sssssshhhhhhhh
app.loop.stop() app.loop.stop()
try: call("sl", shell=True) try: call("sl")
except: pass except: pass
app.loop.start() app.loop.start()
elif keyl == "$": elif keyl == "$":
app.loop.stop() app.loop.stop()
call("clear", shell=True) call("clear")
readline.set_completer(rlcompleter.Completer().complete) readline.set_completer(rlcompleter.Completer().complete)
readline.parse_and_bind("tab: complete") readline.parse_and_bind("tab: complete")
interact(banner="Python " + version + "\nBBJ Interactive Console\nCtrl-D exits.", local=globals()) interact(banner="Python " + version + "\nBBJ Interactive Console\nCtrl-D exits.", local=globals())
@ -2306,7 +2319,7 @@ def frilly_exit():
out = " ~~CoMeE BaCkK SooOn~~ 0000000" out = " ~~CoMeE BaCkK SooOn~~ 0000000"
motherfucking_rainbows(out.zfill(width)) motherfucking_rainbows(out.zfill(width))
else: else:
call("clear", shell=True) call("clear")
motherfucking_rainbows("Come back soon! <3") motherfucking_rainbows("Come back soon! <3")
exit() exit()
@ -2385,47 +2398,57 @@ def paren_prompt(text, positive=True, choices=[], function=input, default=None):
return "" return ""
def sane_value(key, prompt, positive=True, return_empty=False): def sane_value(prompt, validate, positive=True, function=input):
response = paren_prompt(prompt, positive) """Prompts for an input with paren_prompt until validate(response)
if return_empty and response == "": returns None (or something falsy). Otherwise the returned string is used
return response as the new prompt.
try: network.validate(key, response) """
except AssertionError as e: while 1:
return sane_value(key, e.description, False) response = paren_prompt(prompt, positive, function=function)
return response error = validate(response)
if not error:
return response
prompt = str(error) if error != None else ""
positive = False
def password_loop(prompt, positive=True): def password_loop(prompt, positive=True):
response1 = paren_prompt(prompt, positive, function=getpass) while 1:
if response1 == "": response1 = paren_prompt(prompt, positive, function=getpass)
confprompt = "Confirm empty password" if response1 == "":
else: confprompt = "Confirm empty password"
confprompt = "Confirm it" else:
response2 = paren_prompt(confprompt, function=getpass) confprompt = "Confirm it"
if response1 != response2: response2 = paren_prompt(confprompt, function=getpass)
return password_loop("Those didnt match. Try again", False) if response1 != response2:
return response1 prompt = "Those didnt match. Try again"
positive = False
else:
return response1
def nameloop(prompt, positive): def nameloop(network, prompt, positive=True):
name = sane_value("user_name", prompt, positive) def validate_name(name):
if network.user_is_registered(name): try: network.validate("user_name", name)
return nameloop("%s is already registered" % name, False) except AssertionError as e:
return name return e.description
if network.user_is_registered(name):
return "%s is already registered" % name
return sane_value(prompt, validate_name, positive)
def log_in(network, name="", password=""):
def log_in(relog=False):
""" """
Handles login or registration using an oldschool input() Handles login or registration. If name and/or password are not
chain. The user is run through this before starting the provided, the user is prompted for them using an oldschool
input() chain. The user is run through this before starting the
curses app. curses app.
""" """
if relog: if not name:
name = sane_value("user_name", "Username", return_empty=True) def validate_name(response):
else: if response != "": # allow empty username
name = get_arg("user") \ try: network.validate("user_name", response)
or os.getenv("BBJ_USER") \ except AssertionError as e:
or sane_value("user_name", "Username", return_empty=True) return e.description
name = sane_value("Username", validate_name)
if name == "": if name == "":
motherfucking_rainbows("~~W3 4R3 4n0nYm0u5~~") motherfucking_rainbows("~~W3 4R3 4n0nYm0u5~~")
else: else:
@ -2434,21 +2457,18 @@ def log_in(relog=False):
try: try:
network.set_credentials( network.set_credentials(
name, name,
os.getenv("BBJ_PASSWORD", default="") password if name and password else ""
if not relog else ""
) )
# make it easy for people who use an empty password =) # make it easy for people who use an empty password =)
motherfucking_rainbows("~~welcome back {}~~".format(network.user_name)) motherfucking_rainbows("~~welcome back {}~~".format(network.user_name))
except ConnectionRefusedError: except ConnectionRefusedError:
def login_loop(prompt, positive): def validate_creds(password):
try: try:
password = paren_prompt(prompt, positive, function=getpass)
network.set_credentials(name, password) network.set_credentials(name, password)
except ConnectionRefusedError: except ConnectionRefusedError:
login_loop("// R E J E C T E D //.", False) return "// R E J E C T E D //."
password = sane_value("Enter your password", validate_creds, function=getpass)
login_loop("Enter your password", True)
motherfucking_rainbows("~~welcome back {}~~".format(network.user_name)) motherfucking_rainbows("~~welcome back {}~~".format(network.user_name))
except ValueError: except ValueError:
@ -2459,7 +2479,7 @@ def log_in(relog=False):
) )
if response == "c": if response == "c":
name = nameloop("Pick a new name", True) name = nameloop(network, "Pick a new name")
elif response == "n": elif response == "n":
raise InterruptedError raise InterruptedError
@ -2502,11 +2522,16 @@ def bbjrc(mode, **params):
return values return values
def mark(directive=True): def mark(key, value=None, default=None):
""" """
Set and retrieve positional marks for threads. Sets a value in the markfile and returns the old value (or default).
This uses a seperate file from the preferences This uses a seperate file from the preferences
to keep it free from clutter. to keep it free from clutter.
The key must be a string, and the value must be
json-encodable. If value isn't provided (or is None)
then this doesn't set anything and it is only a
read operation.
""" """
try: try:
with open(markpath, "r") as _in: with open(markpath, "r") as _in:
@ -2514,19 +2539,14 @@ def mark(directive=True):
except FileNotFoundError: except FileNotFoundError:
values = {} values = {}
if directive == True and app.mode == "thread": old = values.get(key, default)
pos = app.get_focus_post()
values[app.thread["thread_id"]] = pos if value is not None and value != old:
values[key] = value
with open(markpath, "w") as _out: with open(markpath, "w") as _out:
json.dump(values, _out) json.dump(values, _out)
return pos
elif isinstance(directive, str):
try:
return values[directive]
except KeyError:
return 0
return old
def load_client_pins(): def load_client_pins():
""" """
@ -2562,25 +2582,39 @@ def ignore(*_, **__):
pass pass
def wipe_screen(*_): # XxX_N0_4rgP4rs3_XxX ###yoloswag
""" def get_arg(key, default=None, get_value=True):
A crude hack to repaint the whole screen. I didnt immediately try:
see anything to acheive this in the MainLoop methods so this spec = argv.index("--" + key)
will do, I suppose. value = argv[spec + 1] if get_value else True
""" except ValueError: # --key not specified
app.loop.stop() value = default
call("clear", shell=True) except IndexError: # flag given but no value
app.loop.start() exit("invalid format for --" + key)
return value
def main(): def main():
if get_arg("help", False, False):
print(help_text)
exit()
try:
network = BBJ(get_arg("host", "127.0.0.1"),
get_arg("port", 7099),
get_arg("https", False, False))
except URLError as e:
# print the connection error in red
exit("\033[0;31m%s\033[0m" % repr(e))
global app global app
app = App() app = App(network)
call("clear", shell=True) call("clear")
motherfucking_rainbows(obnoxious_logo) motherfucking_rainbows(obnoxious_logo)
print(welcome) print(welcome)
try: try:
log_in() name = get_arg("user") or os.getenv("BBJ_USER")
password = os.getenv("BBJ_PASSWORD", default="")
log_in(network, name, password)
app.index() app.index()
app.loop.run() app.loop.run()
except (InterruptedError, KeyboardInterrupt): except (InterruptedError, KeyboardInterrupt):