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