fix some typos and warnings
parent
9488ade7fb
commit
1538aad56e
34
README.md
34
README.md
|
@ -5,11 +5,41 @@ miraculously shit out a fully functional, text-driven community bulletin board.
|
||||||
Requires Python 3.4 and up for the server and the official TUI client (clients/urwid/).
|
Requires Python 3.4 and up for the server and the official TUI client (clients/urwid/).
|
||||||
|
|
||||||
![AAAAAAAAAAAAAAAAAAAA](readme.png)
|
![AAAAAAAAAAAAAAAAAAAA](readme.png)
|
||||||
<center><h2>Look Ma, it boots !!11!</h2></center>
|
<div style="text-align: center;"><h2>Look Ma, it boots !!11!</h2></div>
|
||||||
|
|
||||||
It's all driven by an API sitting on top of CherryPy. Currently it does not
|
It's all driven by an API sitting on top of CherryPy. Currently, it does not
|
||||||
serve HTML but this is planned for the (distant?) future.
|
serve HTML but this is planned for the (distant?) future.
|
||||||
|
|
||||||
The two official client implementations are a standalone TUI client for
|
The two official client implementations are a standalone TUI client for
|
||||||
the unix terminal, and GNU Emacs. The API is simple and others are welcome
|
the unix terminal, and GNU Emacs. The API is simple and others are welcome
|
||||||
to join the party at some point.
|
to join the party at some point.
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
1. Make a virtual env
|
||||||
|
```
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run setup.sh
|
||||||
|
```
|
||||||
|
./setup.sh venv/bin/python3
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add systemd service (optional)
|
||||||
|
```
|
||||||
|
cp contrib/bbj.service /etc/systemd/system/
|
||||||
|
$EDITOR /etc/systemd/system/bbj.service
|
||||||
|
systemctl enable --now bbj
|
||||||
|
```
|
||||||
|
Be sure to edit bbj.service with your venv and paths.
|
||||||
|
|
||||||
|
4. Make a client script
|
||||||
|
|
||||||
|
Create a script somewhere in your `$PATH` (I used `/usr/local/bin/bbj`) with the following contents,
|
||||||
|
adapting the path to your install:
|
||||||
|
```shell
|
||||||
|
#!/bin/sh
|
||||||
|
exec /srv/bbj/venv/bin/python3 /srv/bbj/clients/urwid/main.py
|
||||||
|
```
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from urllib.error import URLError
|
import json
|
||||||
import urllib.request as url
|
import urllib.request as url
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from time import time
|
from time import time
|
||||||
import json
|
from urllib.error import URLError
|
||||||
|
|
||||||
|
|
||||||
class BBJ(object):
|
class BBJ(object):
|
||||||
# this module isnt exactly complete. The below description claims
|
# this module isn't exactly complete. The below description claims
|
||||||
# `all of its endpoints are mapped to native methods` though this
|
# `all of its endpoints are mapped to native methods` though this
|
||||||
# is not yet true. The documentation for the API is not yet
|
# is not yet true. The documentation for the API is not yet
|
||||||
# complete, and neither is this client. Currently this module is
|
# complete, and neither is this client. Currently, this module is
|
||||||
# being adapted to fit the needs of the urwid client. As it evolves,
|
# being adapted to fit the needs of the urwid client. As it evolves,
|
||||||
# and the rest of the project evolves, this client will be completed
|
# and the rest of the project evolves, this client will be completed
|
||||||
# and well documented.
|
# and well documented.
|
||||||
|
@ -44,11 +44,12 @@ class BBJ(object):
|
||||||
except UserWarning as e:
|
except UserWarning as e:
|
||||||
assert e.code == 4
|
assert e.code == 4
|
||||||
print(e.description)
|
print(e.description)
|
||||||
# want the raw error object? thats weird, but whatever.
|
# want the raw error object? that's weird, but whatever.
|
||||||
return e.body
|
return e.body
|
||||||
|
|
||||||
See the offical API error documentation for more details.
|
See the official API error documentation for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host="127.0.0.1", port=7099, https=False):
|
def __init__(self, host="127.0.0.1", port=7099, https=False):
|
||||||
"""
|
"""
|
||||||
Optionally takes port and host as kwargs. It will immediately
|
Optionally takes port and host as kwargs. It will immediately
|
||||||
|
@ -80,22 +81,20 @@ class BBJ(object):
|
||||||
except URLError:
|
except URLError:
|
||||||
raise URLError("Cannot connect to %s (is the server down?)" % self.base[0:-2])
|
raise URLError("Cannot connect to %s (is the server down?)" % self.base[0:-2])
|
||||||
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Calling the network object itself is exactly the same as calling
|
Calling the network object itself is exactly the same as calling
|
||||||
it's .request() method.
|
its .request() method.
|
||||||
"""
|
"""
|
||||||
return self.request(*args, **kwargs)
|
return self.request(*args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def _hash(self, string):
|
def _hash(string):
|
||||||
"""
|
"""
|
||||||
Handy function to hash a password and return it.
|
Handy function to hash a password and return it.
|
||||||
"""
|
"""
|
||||||
return sha256(bytes(string, "utf8")).hexdigest()
|
return sha256(bytes(string, "utf8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def request(self, endpoint, **params):
|
def request(self, endpoint, **params):
|
||||||
"""
|
"""
|
||||||
Takes the string endpoint, and a variable number of kwargs
|
Takes the string endpoint, and a variable number of kwargs
|
||||||
|
@ -104,7 +103,7 @@ class BBJ(object):
|
||||||
raised.
|
raised.
|
||||||
|
|
||||||
However, one kwarg is magical here: no_auth. If you include
|
However, one kwarg is magical here: no_auth. If you include
|
||||||
this, its not sent with the request, it just disables the
|
this, it's not sent with the request, it just disables the
|
||||||
sending of auth info when it is available (for more info,
|
sending of auth info when it is available (for more info,
|
||||||
read __init__'s documentation).
|
read __init__'s documentation).
|
||||||
|
|
||||||
|
@ -139,7 +138,6 @@ class BBJ(object):
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def raise_exception(self, error_object):
|
def raise_exception(self, error_object):
|
||||||
"""
|
"""
|
||||||
Takes an API error object and raises the appropriate exception,
|
Takes an API error object and raises the appropriate exception,
|
||||||
|
@ -158,7 +156,7 @@ class BBJ(object):
|
||||||
except UserWarning as e:
|
except UserWarning as e:
|
||||||
assert e.code == 4
|
assert e.code == 4
|
||||||
print(e.description)
|
print(e.description)
|
||||||
# want the raw error object? thats weird, but whatever.
|
# want the raw error object? that's weird, but whatever.
|
||||||
return e.body
|
return e.body
|
||||||
"""
|
"""
|
||||||
description = error_object["description"]
|
description = error_object["description"]
|
||||||
|
@ -178,7 +176,6 @@ class BBJ(object):
|
||||||
e.code, e.description, e.body = code, description, error_object
|
e.code, e.description, e.body = code, description, error_object
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def update_instance_info(self):
|
def update_instance_info(self):
|
||||||
"""
|
"""
|
||||||
Stores configuration info for the connected BBJ server.
|
Stores configuration info for the connected BBJ server.
|
||||||
|
@ -192,10 +189,9 @@ class BBJ(object):
|
||||||
response = self("instance_info")
|
response = self("instance_info")
|
||||||
self.instance_info = response["data"]
|
self.instance_info = response["data"]
|
||||||
|
|
||||||
|
|
||||||
def validate(self, key, value, exception=AssertionError):
|
def validate(self, key, value, exception=AssertionError):
|
||||||
"""
|
"""
|
||||||
Uses the server's db_validate method to verify the validty
|
Uses the server's db_validate method to verify the validity
|
||||||
of `value` by `key`. If it is invalid, kwarg exception (default
|
of `value` by `key`. If it is invalid, kwarg exception (default
|
||||||
AssertionError) is raised with the exception containing the
|
AssertionError) is raised with the exception containing the
|
||||||
attribute .description as the server's reason. Exception can
|
attribute .description as the server's reason. Exception can
|
||||||
|
@ -203,7 +199,7 @@ class BBJ(object):
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
# this will fail bacause the server wont allow newlines in usernames.
|
# this will fail because the server won't allow newlines in usernames.
|
||||||
try:
|
try:
|
||||||
bbj.validate("user_name", "des\nvox")
|
bbj.validate("user_name", "des\nvox")
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
|
@ -229,7 +225,6 @@ class BBJ(object):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def validate_all(self, keys_and_values, exception=AssertionError):
|
def validate_all(self, keys_and_values, exception=AssertionError):
|
||||||
"""
|
"""
|
||||||
Takes a single iterable object as its argument, containing
|
Takes a single iterable object as its argument, containing
|
||||||
|
@ -263,12 +258,11 @@ class BBJ(object):
|
||||||
self.validate(key, value, exception) for key, value in keys_and_values
|
self.validate(key, value, exception) for key, value in keys_and_values
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def set_credentials(self, user_name, user_auth, hash_auth=True, check_validity=True):
|
def set_credentials(self, user_name, user_auth, hash_auth=True, check_validity=True):
|
||||||
"""
|
"""
|
||||||
Internalizes user_name and user_auth. Unless hash_auth=False is
|
Internalizes user_name and user_auth. Unless hash_auth=False is
|
||||||
specified, user_auth is assumed to be an unhashed password
|
specified, user_auth is assumed to be an unhashed password
|
||||||
string and it gets hashed with sha256. If you want to handle
|
string, and it gets hashed with sha256. If you want to handle
|
||||||
hashing yourself, make sure to disable that.
|
hashing yourself, make sure to disable that.
|
||||||
|
|
||||||
Unless check_validity is set to false, the new credentials are
|
Unless check_validity is set to false, the new credentials are
|
||||||
|
@ -290,7 +284,7 @@ class BBJ(object):
|
||||||
except ConnectionRefusedError:
|
except ConnectionRefusedError:
|
||||||
# bad auth info
|
# bad auth info
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# paramter validation failed or the user is not registered
|
# parameter validation failed or the user is not registered
|
||||||
|
|
||||||
# you can handle hashing yourself if you want
|
# you can handle hashing yourself if you want
|
||||||
password = input("Enter your password:")
|
password = input("Enter your password:")
|
||||||
|
@ -312,7 +306,6 @@ class BBJ(object):
|
||||||
self.user = self("get_me")["data"]
|
self.user = self("get_me")["data"]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def validate_credentials(self, user_name, user_auth, exception=True):
|
def validate_credentials(self, user_name, user_auth, exception=True):
|
||||||
"""
|
"""
|
||||||
Pings the server to check that user_name can be authenticated with
|
Pings the server to check that user_name can be authenticated with
|
||||||
|
@ -351,7 +344,6 @@ class BBJ(object):
|
||||||
raise e
|
raise e
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def user_is_registered(self, user_name):
|
def user_is_registered(self, user_name):
|
||||||
"""
|
"""
|
||||||
Returns True or False whether user_name is registered
|
Returns True or False whether user_name is registered
|
||||||
|
@ -365,7 +357,6 @@ class BBJ(object):
|
||||||
|
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def user_register(self, user_name, user_auth, hash_auth=True, set_as_user=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
|
Register user_name into the system with user_auth. Unless hash_auth
|
||||||
|
@ -406,7 +397,6 @@ class BBJ(object):
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def user_update(self, **params):
|
def user_update(self, **params):
|
||||||
"""
|
"""
|
||||||
Update the user's data on the server. The new parameters
|
Update the user's data on the server. The new parameters
|
||||||
|
@ -422,7 +412,6 @@ class BBJ(object):
|
||||||
self.user = self("get_me")["data"]
|
self.user = self("get_me")["data"]
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def user_get(self, user_id_or_name):
|
def user_get(self, user_id_or_name):
|
||||||
"""
|
"""
|
||||||
Return a full user object by their id or username.
|
Return a full user object by their id or username.
|
||||||
|
@ -432,13 +421,12 @@ class BBJ(object):
|
||||||
same objects. You shouldn't use this method when a usermap
|
same objects. You shouldn't use this method when a usermap
|
||||||
is provided.
|
is provided.
|
||||||
|
|
||||||
If the user element isnt found, ValueError is raised.
|
If the user element isn't found, ValueError is raised.
|
||||||
See also `user_is_registered`
|
See also `user_is_registered`
|
||||||
"""
|
"""
|
||||||
response = self("user_get", target_user=user_id_or_name)
|
response = self("user_get", target_user=user_id_or_name)
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def thread_index(self, include_op=False):
|
def thread_index(self, include_op=False):
|
||||||
"""
|
"""
|
||||||
Returns a tuple where [0] is a list of all threads ordered by
|
Returns a tuple where [0] is a list of all threads ordered by
|
||||||
|
@ -453,7 +441,6 @@ class BBJ(object):
|
||||||
response = self("thread_index", include_op=include_op)
|
response = self("thread_index", include_op=include_op)
|
||||||
return response["data"], response["usermap"]
|
return response["data"], response["usermap"]
|
||||||
|
|
||||||
|
|
||||||
def thread_load(self, thread_id, format=None, op_only=False):
|
def thread_load(self, thread_id, format=None, op_only=False):
|
||||||
"""
|
"""
|
||||||
Returns a tuple where [0] is a thread object and [1] is a usermap object.
|
Returns a tuple where [0] is a thread object and [1] is a usermap object.
|
||||||
|
@ -469,7 +456,6 @@ class BBJ(object):
|
||||||
format=format, thread_id=thread_id, op_only=op_only)
|
format=format, thread_id=thread_id, op_only=op_only)
|
||||||
return response["data"], response["usermap"]
|
return response["data"], response["usermap"]
|
||||||
|
|
||||||
|
|
||||||
def thread_create(self, title, body):
|
def thread_create(self, title, body):
|
||||||
"""
|
"""
|
||||||
Submit a new thread, and return its new object. Requires the
|
Submit a new thread, and return its new object. Requires the
|
||||||
|
@ -480,7 +466,6 @@ class BBJ(object):
|
||||||
response = self("thread_create", title=title, body=body)
|
response = self("thread_create", title=title, body=body)
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def thread_reply(self, thread_id, body):
|
def thread_reply(self, thread_id, body):
|
||||||
"""
|
"""
|
||||||
Submits a new reply to a thread and returns the new object.
|
Submits a new reply to a thread and returns the new object.
|
||||||
|
@ -489,10 +474,9 @@ class BBJ(object):
|
||||||
response = self("thread_reply", thread_id=thread_id, body=body)
|
response = self("thread_reply", thread_id=thread_id, body=body)
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def fake_message(self, body="!!", format="sequential", author=None, post_id=0):
|
def fake_message(self, body="!!", format="sequential", author=None, post_id=0):
|
||||||
"""
|
"""
|
||||||
Produce a a valid message object with `body`. Useful for
|
Produce a valid message object with `body`. Useful for
|
||||||
testing and can also be used mimic server messages in a
|
testing and can also be used mimic server messages in a
|
||||||
client.
|
client.
|
||||||
"""
|
"""
|
||||||
|
@ -506,7 +490,6 @@ class BBJ(object):
|
||||||
"thread_id": "gibberish"
|
"thread_id": "gibberish"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_message(self, body, format="sequential"):
|
def format_message(self, body, format="sequential"):
|
||||||
"""
|
"""
|
||||||
Send `body` to the server to be formatted according to `format`,
|
Send `body` to the server to be formatted according to `format`,
|
||||||
|
@ -515,7 +498,6 @@ class BBJ(object):
|
||||||
response = self("format_message", body=body, format=format)
|
response = self("format_message", body=body, format=format)
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def message_delete(self, thread_id, post_id):
|
def message_delete(self, thread_id, post_id):
|
||||||
"""
|
"""
|
||||||
Delete message `post_id` from `thread_id`. The same rules apply
|
Delete message `post_id` from `thread_id`. The same rules apply
|
||||||
|
@ -526,11 +508,10 @@ class BBJ(object):
|
||||||
response = self("delete_post", thread_id=thread_id, post_id=post_id)
|
response = self("delete_post", thread_id=thread_id, post_id=post_id)
|
||||||
return response["data"]
|
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 the database to see if a post can
|
||||||
be edited by the logged in user. thread_id and
|
be edited by the logged-in user. thread_id and
|
||||||
post_id are required.
|
post_id are required.
|
||||||
|
|
||||||
Returns a message object on success, or raises
|
Returns a message object on success, or raises
|
||||||
|
@ -539,11 +520,10 @@ class BBJ(object):
|
||||||
response = self("edit_query", thread_id=thread_id, post_id=int(post_id))
|
response = self("edit_query", thread_id=thread_id, post_id=int(post_id))
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def can_edit(self, thread_id, post_id):
|
def can_edit(self, thread_id, post_id):
|
||||||
"""
|
"""
|
||||||
Return bool True/False that the post at thread_id | post_id
|
Return bool True/False that the post at thread_id | post_id
|
||||||
can be edited by the logged in user. Will not raise UserWarning.
|
can be edited by the logged-in user. Will not raise UserWarning.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = bool(self.edit_query(thread_id, post_id))
|
result = bool(self.edit_query(thread_id, post_id))
|
||||||
|
@ -551,7 +531,6 @@ class BBJ(object):
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def edit_message(self, thread_id, post_id, new_body):
|
def edit_message(self, thread_id, post_id, new_body):
|
||||||
"""
|
"""
|
||||||
Requires the thread_id and post_id. The edit flag is then
|
Requires the thread_id and post_id. The edit flag is then
|
||||||
|
@ -566,7 +545,6 @@ class BBJ(object):
|
||||||
post_id=post_id, body=new_body)
|
post_id=post_id, body=new_body)
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def set_post_raw(self, thread_id, post_id, value):
|
def set_post_raw(self, thread_id, post_id, value):
|
||||||
"""
|
"""
|
||||||
This is a subset of `edit_message` that retains the old
|
This is a subset of `edit_message` that retains the old
|
||||||
|
@ -581,7 +559,6 @@ class BBJ(object):
|
||||||
value=bool(value))
|
value=bool(value))
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def user_is_admin(self, user_name_or_id):
|
def user_is_admin(self, user_name_or_id):
|
||||||
"""
|
"""
|
||||||
Return boolean True or False whether the given user identifier
|
Return boolean True or False whether the given user identifier
|
||||||
|
@ -591,18 +568,16 @@ class BBJ(object):
|
||||||
response = self("is_admin", target_user=user_name_or_id)
|
response = self("is_admin", target_user=user_name_or_id)
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def thread_set_pin(self, thread_id, new_status):
|
def thread_set_pin(self, thread_id, new_status):
|
||||||
"""
|
"""
|
||||||
Set whether a thread should be pinned or not. new_status
|
Set whether a thread should be pinned or not. new_status
|
||||||
is evaluated as a boolean, and given that the logged in
|
is evaluated as a boolean, and given that the logged-in
|
||||||
user is an admin, the thread is set to this status on
|
user is an admin, the thread is set to this status on
|
||||||
the server, and the boolean is returned.
|
the server, and the boolean is returned.
|
||||||
"""
|
"""
|
||||||
response = self("thread_set_pin", thread_id=thread_id, value=new_status)
|
response = self("thread_set_pin", thread_id=thread_id, value=new_status)
|
||||||
return response["data"]
|
return response["data"]
|
||||||
|
|
||||||
|
|
||||||
def message_feed(self, time, format=None):
|
def message_feed(self, time, format=None):
|
||||||
"""
|
"""
|
||||||
Returns a special object representing all activity on the board since
|
Returns a special object representing all activity on the board since
|
||||||
|
@ -628,7 +603,7 @@ class BBJ(object):
|
||||||
objects from the usermap object.
|
objects from the usermap object.
|
||||||
|
|
||||||
The "messages" array is already sorted by submission time, newest
|
The "messages" array is already sorted by submission time, newest
|
||||||
first. The order in the threads object is undefined and you should
|
first. The order in the threads object is undefined, and you should
|
||||||
instead use their `last_mod` attribute if you intend to list them
|
instead use their `last_mod` attribute if you intend to list them
|
||||||
out visually.
|
out visually.
|
||||||
|
|
||||||
|
|
|
@ -3,39 +3,42 @@
|
||||||
If you're looking for help on how to use the program, just press
|
If you're looking for help on how to use the program, just press
|
||||||
? while its running. This mess will not help you.
|
? while its running. This mess will not help you.
|
||||||
|
|
||||||
Urwid aint my speed. Hell, making complex, UI-oriented programs
|
Urwid ain't my speed. Hell, making complex, UI-oriented programs
|
||||||
aint my speed. So some of this code is pretty messy. I stand by
|
ain't my speed. So some of this code is pretty messy. I stand by
|
||||||
it though, and it seems to be working rather well.
|
it though, and it seems to be working rather well.
|
||||||
|
|
||||||
Most of the functionality is crammed in the App() class. Key
|
Most of the functionality is crammed in the App() class. Key
|
||||||
handling is found in the other subclasses for urwid widgets.
|
handling is found in the other subclasses for urwid widgets.
|
||||||
An instantiation of App() is casted as `app` globally and
|
An instantiation of App() is cast as `app` globally and
|
||||||
the keypress methods will call into this global `app` object.
|
the keypress methods will call into this global `app` object.
|
||||||
|
|
||||||
There are few additional functions that are defined outside
|
There are few additional functions that are defined outside
|
||||||
of the App class. They are delegated to the very bottom of
|
the App class. They are delegated to the very bottom of
|
||||||
this file.
|
this file.
|
||||||
|
|
||||||
Please mail me (~desvox) for feedback and for any of your
|
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 string import punctuation
|
|
||||||
from datetime import datetime
|
|
||||||
from sys import argv, version
|
|
||||||
from time import time, sleep
|
|
||||||
from getpass import getpass
|
|
||||||
from subprocess import call
|
|
||||||
from random import choice
|
|
||||||
from code import interact
|
|
||||||
import rlcompleter
|
|
||||||
import readline
|
|
||||||
import tempfile
|
|
||||||
import urwid
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import readline
|
||||||
|
import rlcompleter
|
||||||
|
import tempfile
|
||||||
|
from code import interact
|
||||||
|
from datetime import datetime
|
||||||
|
from getpass import getpass
|
||||||
|
from random import choice
|
||||||
|
from string import punctuation
|
||||||
|
from subprocess import call
|
||||||
|
from sys import argv, version
|
||||||
|
from time import time, sleep
|
||||||
|
|
||||||
|
import urwid
|
||||||
|
|
||||||
|
from network import BBJ, URLError
|
||||||
|
|
||||||
|
|
||||||
# XxX_N0_4rgP4rs3_XxX ###yoloswag
|
# XxX_N0_4rgP4rs3_XxX ###yoloswag
|
||||||
def get_arg(key, default=None, get_value=True):
|
def get_arg(key, default=None, get_value=True):
|
||||||
|
@ -48,6 +51,7 @@ def get_arg(key, default=None, get_value=True):
|
||||||
exit("invalid format for --" + key)
|
exit("invalid format for --" + key)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
if get_arg("help", False, False):
|
if get_arg("help", False, False):
|
||||||
print("""BBJ Urwid Client
|
print("""BBJ Urwid Client
|
||||||
Available options:
|
Available options:
|
||||||
|
@ -124,10 +128,9 @@ format_help = [
|
||||||
"long as you are not anonymous.",
|
"long as you are not anonymous.",
|
||||||
|
|
||||||
"In previous versions of BBJ, linebreaks were joined into sentences if they "
|
"In previous versions of BBJ, linebreaks were joined into sentences if they "
|
||||||
"occured in the same paragraph, however this confused many users and has been "
|
"occurred in the same paragraph, however this confused many users and has been "
|
||||||
"reverted to just use whatever was submitted, as-is.",
|
"reverted to just use whatever was submitted, as-is.",
|
||||||
|
|
||||||
|
|
||||||
"[red: Colors, Bold, Underline & Expressions]",
|
"[red: Colors, Bold, Underline & Expressions]",
|
||||||
|
|
||||||
"You can use [rainbow: rainbow], [red: red], [yellow: yellow], [green: green], "
|
"You can use [rainbow: rainbow], [red: red], [yellow: yellow], [green: green], "
|
||||||
|
@ -146,7 +149,7 @@ format_help = [
|
||||||
"closing brackets need to be escaped within an expression. Any backslashes used "
|
"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.",
|
"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] "
|
"This peculiar syntax eliminates false positives. You never have to escape [normal] "
|
||||||
"brackets when using the board. Only expressions with **valid and defined** directives "
|
"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 "
|
"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.",
|
"not a directive. [red this will pass too] because the colon is missing.",
|
||||||
|
@ -161,7 +164,7 @@ format_help = [
|
||||||
"You can refer to a post number using two angle brackets pointing into a number. >>432 "
|
"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 "
|
"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, "
|
"this violates the sentence structure outlined in the **Whitespace** section above, "
|
||||||
"so you may introduce >greentext without splicing into seperate paragraphs. The '>' "
|
"so you may introduce >greentext without splicing into separate paragraphs. The '>' "
|
||||||
"must be the first character on the line with no whitespace before it.\n>it looks like this\n"
|
"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. The formatter is smart enough to "
|
"and the paragraph doesnt have to break on either side. The formatter is smart enough to "
|
||||||
"differentiate between >>greentext with multiple arrows and numeric quotes (outlined below) "
|
"differentiate between >>greentext with multiple arrows and numeric quotes (outlined below) "
|
||||||
|
@ -188,7 +191,8 @@ general_help = [
|
||||||
"To make scrolling faster, ", ("button", "hold shift"), " when using a control: it "
|
"To make scrolling faster, ", ("button", "hold shift"), " when using a control: it "
|
||||||
"will repeat 5 times by default, and you can change this number in your settings.\n\n"
|
"will repeat 5 times by default, and you can change this number in your settings.\n\n"
|
||||||
|
|
||||||
"In threads, The ", ("button", "<"), " and ", ("button", ">"), " keys will jump by "
|
"In threads, The ", ("button", "<"), " and ",
|
||||||
|
("button", ">"), " keys will jump by "
|
||||||
"a chosen number of post headers. You can see the count inside of the footer line at "
|
"a chosen number of post headers. You can see the count inside of the footer line at "
|
||||||
"the far right side: press ", ("button", "x"), " to cycle it upwards or ",
|
"the far right side: press ", ("button", "x"), " to cycle it upwards or ",
|
||||||
("button", "X"), " to cycle it downwards.\n\n"
|
("button", "X"), " to cycle it downwards.\n\n"
|
||||||
|
@ -327,6 +331,7 @@ rcpath = os.path.join(os.getenv("HOME"), ".bbjrc")
|
||||||
markpath = os.path.join(os.getenv("HOME"), ".bbjmarks")
|
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):
|
||||||
self.prefs = bbjrc("load")
|
self.prefs = bbjrc("load")
|
||||||
|
@ -368,7 +373,6 @@ class App(object):
|
||||||
palette=colormap,
|
palette=colormap,
|
||||||
handle_mouse=self.prefs["mouse_integration"])
|
handle_mouse=self.prefs["mouse_integration"])
|
||||||
|
|
||||||
|
|
||||||
def frame_theme(self, title=""):
|
def frame_theme(self, title=""):
|
||||||
"""
|
"""
|
||||||
Return the kwargs for a frame theme.
|
Return the kwargs for a frame theme.
|
||||||
|
@ -379,10 +383,9 @@ class App(object):
|
||||||
theme.update({"title": title})
|
theme.update({"title": title})
|
||||||
return theme
|
return theme
|
||||||
|
|
||||||
|
|
||||||
def set_header(self, text, *format_specs):
|
def set_header(self, text, *format_specs):
|
||||||
"""
|
"""
|
||||||
Update the header line with the logged in user, a seperator,
|
Update the header line with the logged-in user, a seperator,
|
||||||
then concat text with format_specs applied to it. Applies
|
then concat text with format_specs applied to it. Applies
|
||||||
bar formatting to it.
|
bar formatting to it.
|
||||||
"""
|
"""
|
||||||
|
@ -392,7 +395,6 @@ class App(object):
|
||||||
)
|
)
|
||||||
self.loop.widget.header = urwid.AttrMap(urwid.Text(header), "bar")
|
self.loop.widget.header = urwid.AttrMap(urwid.Text(header), "bar")
|
||||||
|
|
||||||
|
|
||||||
def set_footer(self, string):
|
def set_footer(self, string):
|
||||||
"""
|
"""
|
||||||
Sets the footer to display `string`, applying bar formatting.
|
Sets the footer to display `string`, applying bar formatting.
|
||||||
|
@ -405,7 +407,6 @@ class App(object):
|
||||||
# self.loop.widget.footer[0].set_text(widget)
|
# self.loop.widget.footer[0].set_text(widget)
|
||||||
# else:
|
# else:
|
||||||
|
|
||||||
|
|
||||||
def set_default_header(self):
|
def set_default_header(self):
|
||||||
"""
|
"""
|
||||||
Sets the header to the default for the current screen.
|
Sets the header to the default for the current screen.
|
||||||
|
@ -416,7 +417,6 @@ class App(object):
|
||||||
else:
|
else:
|
||||||
self.set_header("{} threads", len(self.walker))
|
self.set_header("{} threads", len(self.walker))
|
||||||
|
|
||||||
|
|
||||||
def set_default_footer(self, clobber_composer=False):
|
def set_default_footer(self, clobber_composer=False):
|
||||||
"""
|
"""
|
||||||
Sets the footer to the default for the current screen.
|
Sets the footer to the default for the current screen.
|
||||||
|
@ -434,7 +434,6 @@ class App(object):
|
||||||
|
|
||||||
self.set_footer(footer)
|
self.set_footer(footer)
|
||||||
|
|
||||||
|
|
||||||
def set_bars(self, clobber_composer=False):
|
def set_bars(self, clobber_composer=False):
|
||||||
"""
|
"""
|
||||||
Sets both the footer and header to their default values
|
Sets both the footer and header to their default values
|
||||||
|
@ -443,7 +442,6 @@ class App(object):
|
||||||
self.set_default_header()
|
self.set_default_header()
|
||||||
self.set_default_footer(clobber_composer)
|
self.set_default_footer(clobber_composer)
|
||||||
|
|
||||||
|
|
||||||
def close_editor(self):
|
def close_editor(self):
|
||||||
"""
|
"""
|
||||||
Close whatever editing widget is open and restore proper
|
Close whatever editing widget is open and restore proper
|
||||||
|
@ -457,14 +455,12 @@ class App(object):
|
||||||
self.loop.widget = self.loop.widget[0]
|
self.loop.widget = self.loop.widget[0]
|
||||||
self.set_default_header()
|
self.set_default_header()
|
||||||
|
|
||||||
|
|
||||||
def overlay_p(self):
|
def overlay_p(self):
|
||||||
"""
|
"""
|
||||||
Return True or False if the current widget is an overlay.
|
Return True or False if the current widget is an overlay.
|
||||||
"""
|
"""
|
||||||
return isinstance(self.loop.widget, urwid.Overlay)
|
return isinstance(self.loop.widget, urwid.Overlay)
|
||||||
|
|
||||||
|
|
||||||
def remove_overlays(self, *_):
|
def remove_overlays(self, *_):
|
||||||
"""
|
"""
|
||||||
Remove ALL urwid.Overlay objects which are currently covering the base
|
Remove ALL urwid.Overlay objects which are currently covering the base
|
||||||
|
@ -476,7 +472,6 @@ class App(object):
|
||||||
except:
|
except:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def switch_editor(self):
|
def switch_editor(self):
|
||||||
"""
|
"""
|
||||||
Switch focus between the thread viewer and the open editor
|
Switch focus between the thread viewer and the open editor
|
||||||
|
@ -511,7 +506,6 @@ class App(object):
|
||||||
self.loop.widget.header.attr_map = {None: attr[1]}
|
self.loop.widget.header.attr_map = {None: attr[1]}
|
||||||
self.body.attr_map = {None: attr[1]}
|
self.body.attr_map = {None: attr[1]}
|
||||||
|
|
||||||
|
|
||||||
def readable_delta(self, modified):
|
def readable_delta(self, modified):
|
||||||
"""
|
"""
|
||||||
Return a human-readable string representing the difference
|
Return a human-readable string representing the difference
|
||||||
|
@ -530,7 +524,6 @@ class App(object):
|
||||||
return "%d minutes ago" % minutes
|
return "%d minutes ago" % minutes
|
||||||
return "less than a minute ago"
|
return "less than a minute ago"
|
||||||
|
|
||||||
|
|
||||||
def quote_view_action(self, button, message):
|
def quote_view_action(self, button, message):
|
||||||
"""
|
"""
|
||||||
Callback function to view a quote from the message object menu.
|
Callback function to view a quote from the message object menu.
|
||||||
|
@ -548,7 +541,6 @@ class App(object):
|
||||||
height=("relative", 60)
|
height=("relative", 60)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def quote_view_menu(self, button, post_ids):
|
def quote_view_menu(self, button, post_ids):
|
||||||
"""
|
"""
|
||||||
Receives a list of quote ids and makes a frilly menu to pick one to view.
|
Receives a list of quote ids and makes a frilly menu to pick one to view.
|
||||||
|
@ -586,7 +578,6 @@ class App(object):
|
||||||
width=30
|
width=30
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def edit_post(self, button, message):
|
def edit_post(self, button, message):
|
||||||
post_id = message["post_id"]
|
post_id = message["post_id"]
|
||||||
thread_id = message["thread_id"]
|
thread_id = message["thread_id"]
|
||||||
|
@ -600,12 +591,10 @@ class App(object):
|
||||||
self.remove_overlays()
|
self.remove_overlays()
|
||||||
self.compose(init_body=message["body"], edit=message)
|
self.compose(init_body=message["body"], edit=message)
|
||||||
|
|
||||||
|
|
||||||
def reply(self, button, message):
|
def reply(self, button, message):
|
||||||
self.remove_overlays()
|
self.remove_overlays()
|
||||||
self.compose(init_body=">>%d\n\n" % message["post_id"])
|
self.compose(init_body=">>%d\n\n" % message["post_id"])
|
||||||
|
|
||||||
|
|
||||||
def deletion_dialog(self, button, message):
|
def deletion_dialog(self, button, message):
|
||||||
"""
|
"""
|
||||||
Prompts the user to confirm deletion of an item.
|
Prompts the user to confirm deletion of an item.
|
||||||
|
@ -634,14 +623,12 @@ class App(object):
|
||||||
valign=("relative", 50),
|
valign=("relative", 50),
|
||||||
width=30, height=6)
|
width=30, height=6)
|
||||||
|
|
||||||
|
|
||||||
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)
|
network.set_post_raw(message["thread_id"], message["post_id"], raw)
|
||||||
return self.refresh()
|
return self.refresh()
|
||||||
|
|
||||||
|
|
||||||
def on_post(self, button, message):
|
def on_post(self, button, message):
|
||||||
quotes = self.get_quotes(message)
|
quotes = self.get_quotes(message)
|
||||||
author = self.usermap[message["author"]]
|
author = self.usermap[message["author"]]
|
||||||
|
@ -660,7 +647,8 @@ class App(object):
|
||||||
|
|
||||||
if message["post_id"] == 0:
|
if message["post_id"] == 0:
|
||||||
msg = "Thread"
|
msg = "Thread"
|
||||||
else: msg = "Post"
|
else:
|
||||||
|
msg = "Post"
|
||||||
|
|
||||||
raw = message["send_raw"]
|
raw = message["send_raw"]
|
||||||
buttons.insert(0, urwid.Button("Delete %s" % msg, self.deletion_dialog, message))
|
buttons.insert(0, urwid.Button("Delete %s" % msg, self.deletion_dialog, message))
|
||||||
|
@ -689,7 +677,6 @@ class App(object):
|
||||||
height=len(buttons) + 2
|
height=len(buttons) + 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_quotes(self, msg_object, value_type=int):
|
def get_quotes(self, msg_object, value_type=int):
|
||||||
"""
|
"""
|
||||||
Returns the post_ids that msg_object is quoting.
|
Returns the post_ids that msg_object is quoting.
|
||||||
|
@ -704,7 +691,6 @@ class App(object):
|
||||||
[quotes.append(cdr) for car, cdr in paragraph if car == "quote"]
|
[quotes.append(cdr) for car, cdr in paragraph if car == "quote"]
|
||||||
return [value_type(q) for q in quotes]
|
return [value_type(q) for q in quotes]
|
||||||
|
|
||||||
|
|
||||||
def make_thread_body(self, thread, pinned=False):
|
def make_thread_body(self, thread, pinned=False):
|
||||||
"""
|
"""
|
||||||
Returns the pile widget that comprises a thread in the index.
|
Returns the pile widget that comprises a thread in the index.
|
||||||
|
@ -746,7 +732,6 @@ class App(object):
|
||||||
pile.thread = thread
|
pile.thread = thread
|
||||||
return pile
|
return pile
|
||||||
|
|
||||||
|
|
||||||
def make_message_body(self, message, no_action=False):
|
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
|
||||||
|
@ -786,7 +771,6 @@ class App(object):
|
||||||
urwid.AttrMap(urwid.Divider(self.theme["divider"]), "dim")
|
urwid.AttrMap(urwid.Divider(self.theme["divider"]), "dim")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def timestring(self, epoch, mode="both"):
|
def timestring(self, epoch, mode="both"):
|
||||||
"""
|
"""
|
||||||
Returns a string of time representing a given epoch and mode.
|
Returns a string of time representing a given epoch and mode.
|
||||||
|
@ -803,7 +787,6 @@ class App(object):
|
||||||
directive = "%s %s" % (self.prefs["time"], self.prefs["date"])
|
directive = "%s %s" % (self.prefs["time"], self.prefs["date"])
|
||||||
return date.strftime(directive)
|
return date.strftime(directive)
|
||||||
|
|
||||||
|
|
||||||
def index(self, *_, threads=None):
|
def index(self, *_, threads=None):
|
||||||
"""
|
"""
|
||||||
Browse or return to the index.
|
Browse or return to the index.
|
||||||
|
@ -818,7 +801,7 @@ class App(object):
|
||||||
self.window_split = False
|
self.window_split = False
|
||||||
if threads:
|
if threads:
|
||||||
# passing in an argument for threads implies that we are showing a
|
# passing in an argument for threads implies that we are showing a
|
||||||
# narrowed selection of content, so we dont want to resume last_index_pos
|
# narrowed selection of content, so we don't 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 = network.thread_index()
|
||||||
|
@ -852,8 +835,6 @@ class App(object):
|
||||||
# checks to make sure there are any posts to focus
|
# checks to make sure there are any posts to focus
|
||||||
self.box.set_focus(0)
|
self.box.set_focus(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def thread_load(self, button, thread_id):
|
def thread_load(self, button, thread_id):
|
||||||
"""
|
"""
|
||||||
Open a thread.
|
Open a thread.
|
||||||
|
@ -878,7 +859,6 @@ class App(object):
|
||||||
self.set_default_footer()
|
self.set_default_footer()
|
||||||
self.goto_post(mark(thread_id))
|
self.goto_post(mark(thread_id))
|
||||||
|
|
||||||
|
|
||||||
def toggle_client_pin(self):
|
def toggle_client_pin(self):
|
||||||
if self.mode != "index":
|
if self.mode != "index":
|
||||||
return
|
return
|
||||||
|
@ -886,7 +866,6 @@ class App(object):
|
||||||
self.client_pinned_threads = toggle_client_pin(thread_id)
|
self.client_pinned_threads = toggle_client_pin(thread_id)
|
||||||
self.index()
|
self.index()
|
||||||
|
|
||||||
|
|
||||||
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 network.user["is_admin"]:
|
||||||
return
|
return
|
||||||
|
@ -894,7 +873,6 @@ class App(object):
|
||||||
network.thread_set_pin(thread["thread_id"], not thread["pinned"])
|
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 = network.thread_index()
|
||||||
|
@ -910,9 +888,8 @@ class App(object):
|
||||||
else:
|
else:
|
||||||
self.temp_footer_message("No results for '{}'".format(query))
|
self.temp_footer_message("No results for '{}'".format(query))
|
||||||
|
|
||||||
|
|
||||||
def search_thread_callback(self, query):
|
def search_thread_callback(self, query):
|
||||||
# normally i would just use self.thread["messages"] but I need the visbile text post-formatted
|
# normally i would just use self.thread["messages"] but I need the visible text post-formatted
|
||||||
query = query.lower().strip()
|
query = query.lower().strip()
|
||||||
self.match_data["matches"] = [
|
self.match_data["matches"] = [
|
||||||
self.thread["messages"][widget.base_widget.post_id] for widget in self.walker
|
self.thread["messages"][widget.base_widget.post_id] for widget in self.walker
|
||||||
|
@ -927,7 +904,6 @@ class App(object):
|
||||||
else:
|
else:
|
||||||
self.temp_footer_message("No results for '{}'".format(query))
|
self.temp_footer_message("No results for '{}'".format(query))
|
||||||
|
|
||||||
|
|
||||||
def do_search_result(self, forward=True):
|
def do_search_result(self, forward=True):
|
||||||
if not self.match_data["matches"]:
|
if not self.match_data["matches"]:
|
||||||
return
|
return
|
||||||
|
@ -945,7 +921,6 @@ class App(object):
|
||||||
self.match_data["position"] + 1, length, self.match_data["query"]
|
self.match_data["position"] + 1, length, self.match_data["query"]
|
||||||
), 5)
|
), 5)
|
||||||
|
|
||||||
|
|
||||||
# XXX: Try to find a way to overlay properties onto an existing widget instead of this trainwreck.
|
# XXX: Try to find a way to overlay properties onto an existing widget instead of this trainwreck.
|
||||||
# def highlight_query(self):
|
# def highlight_query(self):
|
||||||
# # pass
|
# # pass
|
||||||
|
@ -967,7 +942,6 @@ class App(object):
|
||||||
# index += length
|
# index += length
|
||||||
# new_attrs.append((prop, length))
|
# new_attrs.append((prop, length))
|
||||||
|
|
||||||
|
|
||||||
def search_prompt(self):
|
def search_prompt(self):
|
||||||
if self.mode == "index":
|
if self.mode == "index":
|
||||||
callback = self.search_index_callback
|
callback = self.search_index_callback
|
||||||
|
@ -991,7 +965,6 @@ class App(object):
|
||||||
valign=("relative", 25 if self.window_split else 50),
|
valign=("relative", 25 if self.window_split else 50),
|
||||||
width=("relative", 40), height=6)
|
width=("relative", 40), height=6)
|
||||||
|
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self.remove_overlays()
|
self.remove_overlays()
|
||||||
if self.mode == "index":
|
if self.mode == "index":
|
||||||
|
@ -1006,7 +979,6 @@ class App(object):
|
||||||
self.goto_post(mark(thread))
|
self.goto_post(mark(thread))
|
||||||
self.temp_footer_message("Refreshed content!", 1)
|
self.temp_footer_message("Refreshed content!", 1)
|
||||||
|
|
||||||
|
|
||||||
def back(self, terminate=False):
|
def back(self, terminate=False):
|
||||||
if app.mode == "index" and terminate:
|
if app.mode == "index" and terminate:
|
||||||
frilly_exit()
|
frilly_exit()
|
||||||
|
@ -1041,14 +1013,12 @@ class App(object):
|
||||||
mark()
|
mark()
|
||||||
self.index()
|
self.index()
|
||||||
|
|
||||||
|
|
||||||
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":
|
||||||
return (pos - (pos % 5)) // 5
|
return (pos - (pos % 5)) // 5
|
||||||
return pos if not return_widget else self.walker[pos]
|
return pos if not return_widget else self.walker[pos]
|
||||||
|
|
||||||
|
|
||||||
def header_jump_next(self):
|
def header_jump_next(self):
|
||||||
if self.mode == "index":
|
if self.mode == "index":
|
||||||
return self.box.keypress(self.loop.screen_size, "down")
|
return self.box.keypress(self.loop.screen_size, "down")
|
||||||
|
@ -1056,8 +1026,8 @@ class App(object):
|
||||||
post = self.get_focus_post()
|
post = self.get_focus_post()
|
||||||
if post != self.thread["reply_count"]:
|
if post != self.thread["reply_count"]:
|
||||||
self.goto_post(post + 1)
|
self.goto_post(post + 1)
|
||||||
else: break
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
def header_jump_previous(self):
|
def header_jump_previous(self):
|
||||||
if self.mode == "index":
|
if self.mode == "index":
|
||||||
|
@ -1066,8 +1036,8 @@ class App(object):
|
||||||
post = self.get_focus_post()
|
post = self.get_focus_post()
|
||||||
if post != 0:
|
if post != 0:
|
||||||
self.goto_post(post - 1)
|
self.goto_post(post - 1)
|
||||||
else: break
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
def goto_post(self, number):
|
def goto_post(self, number):
|
||||||
if self.mode != "thread":
|
if self.mode != "thread":
|
||||||
|
@ -1084,7 +1054,6 @@ class App(object):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self.temp_footer_message("OUT OF BOUNDS")
|
self.temp_footer_message("OUT OF BOUNDS")
|
||||||
|
|
||||||
|
|
||||||
def goto_post_prompt(self, init):
|
def goto_post_prompt(self, init):
|
||||||
if self.mode != "thread":
|
if self.mode != "thread":
|
||||||
return
|
return
|
||||||
|
@ -1113,7 +1082,6 @@ class App(object):
|
||||||
valign=("relative", 25 if self.window_split else 50),
|
valign=("relative", 25 if self.window_split else 50),
|
||||||
width=20, height=6)
|
width=20, height=6)
|
||||||
|
|
||||||
|
|
||||||
def jump_peek(self, editor, value, display):
|
def jump_peek(self, editor, value, display):
|
||||||
if not value:
|
if not value:
|
||||||
return display.set_text("")
|
return display.set_text("")
|
||||||
|
@ -1121,24 +1089,22 @@ class App(object):
|
||||||
author = self.usermap[msg["author"]]
|
author = self.usermap[msg["author"]]
|
||||||
display.set_text((str(author["color"]), ">>%s %s" % (value, author["user_name"])))
|
display.set_text((str(author["color"]), ">>%s %s" % (value, author["user_name"])))
|
||||||
|
|
||||||
|
|
||||||
def set_theme(self, button, new_state):
|
def set_theme(self, button, new_state):
|
||||||
"""
|
"""
|
||||||
Callback for the theme radio buttons in the options.
|
Callback for the theme radio buttons in the options.
|
||||||
"""
|
"""
|
||||||
if new_state == True:
|
if new_state:
|
||||||
self.theme = themes[button.label].copy()
|
self.theme = themes[button.label].copy()
|
||||||
if self.prefs["custom_divider_char"]:
|
if self.prefs["custom_divider_char"]:
|
||||||
self.theme["divider"] = self.prefs["custom_divider_char"]
|
self.theme["divider"] = self.prefs["custom_divider_char"]
|
||||||
self.prefs["frame_theme"] = button.label
|
self.prefs["frame_theme"] = button.label
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def set_new_editor(self, button, value, arg):
|
def set_new_editor(self, button, value, arg):
|
||||||
"""
|
"""
|
||||||
Callback for the option radio buttons to set the the text editor.
|
Callback for the option radio buttons to set the text editor.
|
||||||
"""
|
"""
|
||||||
if value == False:
|
if not value:
|
||||||
return
|
return
|
||||||
elif isinstance(value, str):
|
elif isinstance(value, str):
|
||||||
[button.set_state(False) for button in arg]
|
[button.set_state(False) for button in arg]
|
||||||
|
@ -1151,7 +1117,6 @@ class App(object):
|
||||||
self.prefs.update({"editor": key})
|
self.prefs.update({"editor": key})
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def set_editor_mode(self, button, value):
|
def set_editor_mode(self, button, value):
|
||||||
"""
|
"""
|
||||||
Callback for the editor mode radio buttons in the options.
|
Callback for the editor mode radio buttons in the options.
|
||||||
|
@ -1159,11 +1124,9 @@ class App(object):
|
||||||
self.prefs["integrate_external_editor"] = value
|
self.prefs["integrate_external_editor"] = value
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def toggle_thread_pin(self, thread_id):
|
def toggle_thread_pin(self, thread_id):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def relog(self, *_, **__):
|
def relog(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
Options menu callback to log the user in again.
|
Options menu callback to log the user in again.
|
||||||
|
@ -1182,7 +1145,6 @@ class App(object):
|
||||||
self.set_default_header()
|
self.set_default_header()
|
||||||
self.options_menu()
|
self.options_menu()
|
||||||
|
|
||||||
|
|
||||||
def unlog(self, *_, **__):
|
def unlog(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
Options menu callback to anonymize the user and
|
Options menu callback to anonymize the user and
|
||||||
|
@ -1194,10 +1156,9 @@ class App(object):
|
||||||
self.set_default_header()
|
self.set_default_header()
|
||||||
self.options_menu()
|
self.options_menu()
|
||||||
|
|
||||||
|
|
||||||
def general_help(self):
|
def general_help(self):
|
||||||
"""
|
"""
|
||||||
Show a general help dialog. In all honestly, its not
|
Show a general help dialog. In all honestly, it's not
|
||||||
very useful and will only help people who have never
|
very useful and will only help people who have never
|
||||||
really used terminal software before =)
|
really used terminal software before =)
|
||||||
"""
|
"""
|
||||||
|
@ -1224,7 +1185,6 @@ class App(object):
|
||||||
height=("relative", 60)
|
height=("relative", 60)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def formatting_help(self, *_):
|
def formatting_help(self, *_):
|
||||||
"""
|
"""
|
||||||
Pops a help window for formatting directives.
|
Pops a help window for formatting directives.
|
||||||
|
@ -1249,35 +1209,29 @@ class App(object):
|
||||||
height=("relative", vh)
|
height=("relative", vh)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_color(self, button, value, color):
|
def set_color(self, button, value, color):
|
||||||
if value == False:
|
if not value:
|
||||||
return
|
return
|
||||||
network.user_update(color=color)
|
network.user_update(color=color)
|
||||||
|
|
||||||
|
|
||||||
def toggle_exit(self, button, value):
|
def toggle_exit(self, button, value):
|
||||||
self.prefs["dramatic_exit"] = value
|
self.prefs["dramatic_exit"] = value
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def toggle_anon_warn(self, button, value):
|
def toggle_anon_warn(self, button, value):
|
||||||
self.prefs["confirm_anon"] = value
|
self.prefs["confirm_anon"] = value
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def toggle_mouse(self, button, value):
|
def toggle_mouse(self, button, value):
|
||||||
self.prefs["mouse_integration"] = value
|
self.prefs["mouse_integration"] = value
|
||||||
self.loop.handle_mouse = value
|
self.loop.handle_mouse = value
|
||||||
self.loop.screen.set_mouse_tracking(value)
|
self.loop.screen.set_mouse_tracking(value)
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def toggle_spacing(self, button, value):
|
def toggle_spacing(self, button, value):
|
||||||
self.prefs["index_spacing"] = value
|
self.prefs["index_spacing"] = value
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def change_username(self, *_):
|
def change_username(self, *_):
|
||||||
self.loop.stop()
|
self.loop.stop()
|
||||||
call("clear", shell=True)
|
call("clear", shell=True)
|
||||||
|
@ -1293,7 +1247,6 @@ class App(object):
|
||||||
except (KeyboardInterrupt, InterruptedError):
|
except (KeyboardInterrupt, InterruptedError):
|
||||||
self.loop.start()
|
self.loop.start()
|
||||||
|
|
||||||
|
|
||||||
def change_password(self, *_):
|
def change_password(self, *_):
|
||||||
self.loop.stop()
|
self.loop.stop()
|
||||||
call("clear", shell=True)
|
call("clear", shell=True)
|
||||||
|
@ -1309,7 +1262,6 @@ class App(object):
|
||||||
except (KeyboardInterrupt, InterruptedError):
|
except (KeyboardInterrupt, InterruptedError):
|
||||||
self.loop.start()
|
self.loop.start()
|
||||||
|
|
||||||
|
|
||||||
def live_time_render(self, editor, text, args):
|
def live_time_render(self, editor, text, args):
|
||||||
widget, key = args
|
widget, key = args
|
||||||
try:
|
try:
|
||||||
|
@ -1320,25 +1272,22 @@ class App(object):
|
||||||
rendered = ("1", "Invalid Input")
|
rendered = ("1", "Invalid Input")
|
||||||
widget.set_text(rendered)
|
widget.set_text(rendered)
|
||||||
|
|
||||||
|
|
||||||
def edit_width(self, editor, content):
|
def edit_width(self, editor, content):
|
||||||
value = int(content) if content else 0
|
value = int(content) if content else 0
|
||||||
if value < 10: value = 10
|
if value < 10:
|
||||||
|
value = 10
|
||||||
self.prefs["max_text_width"] = value
|
self.prefs["max_text_width"] = value
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def edit_shift(self, editor, content):
|
def edit_shift(self, editor, content):
|
||||||
self.prefs["shift_multiplier"] = \
|
self.prefs["shift_multiplier"] = \
|
||||||
int(content) if content else 0
|
int(content) if content else 0
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def save_escape_key(self, value, mode):
|
def save_escape_key(self, value, mode):
|
||||||
self.prefs["edit_escapes"].update({mode[0]: value})
|
self.prefs["edit_escapes"].update({mode[0]: value})
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def set_escape_key(self, button, args):
|
def set_escape_key(self, button, args):
|
||||||
mode = args[0]
|
mode = args[0]
|
||||||
widget = OptionsMenu(
|
widget = OptionsMenu(
|
||||||
|
@ -1360,7 +1309,6 @@ class App(object):
|
||||||
width=25, height=5
|
width=25, height=5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def incr_jump(self):
|
def incr_jump(self):
|
||||||
if self.mode != "thread":
|
if self.mode != "thread":
|
||||||
return
|
return
|
||||||
|
@ -1373,7 +1321,6 @@ class App(object):
|
||||||
self.set_default_footer()
|
self.set_default_footer()
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def decr_jump(self):
|
def decr_jump(self):
|
||||||
if self.mode != "thread":
|
if self.mode != "thread":
|
||||||
return
|
return
|
||||||
|
@ -1386,7 +1333,6 @@ class App(object):
|
||||||
self.set_default_footer()
|
self.set_default_footer()
|
||||||
bbjrc("update", **self.prefs)
|
bbjrc("update", **self.prefs)
|
||||||
|
|
||||||
|
|
||||||
def options_menu(self):
|
def options_menu(self):
|
||||||
"""
|
"""
|
||||||
Create a popup for the user to configure their account and
|
Create a popup for the user to configure their account and
|
||||||
|
@ -1570,7 +1516,6 @@ class App(object):
|
||||||
height=("relative", 75)
|
height=("relative", 75)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def footer_prompt(self, text, callback, *callback_args, extra_text=None):
|
def footer_prompt(self, text, callback, *callback_args, extra_text=None):
|
||||||
text = "(%s)> " % text
|
text = "(%s)> " % text
|
||||||
widget = urwid.Columns([
|
widget = urwid.Columns([
|
||||||
|
@ -1587,7 +1532,6 @@ class App(object):
|
||||||
self.loop.widget.footer = widget
|
self.loop.widget.footer = widget
|
||||||
self.loop.widget.focus_position = "footer"
|
self.loop.widget.focus_position = "footer"
|
||||||
|
|
||||||
|
|
||||||
def reset_footer(self, *_):
|
def reset_footer(self, *_):
|
||||||
if self.window_split:
|
if self.window_split:
|
||||||
return
|
return
|
||||||
|
@ -1598,7 +1542,6 @@ class App(object):
|
||||||
# just keep trying until the focus widget can handle it
|
# just keep trying until the focus widget can handle it
|
||||||
# return self.loop.set_alarm_in(0.25, self.reset_footer)
|
# return self.loop.set_alarm_in(0.25, self.reset_footer)
|
||||||
|
|
||||||
|
|
||||||
def temp_footer_message(self, string, duration=3):
|
def temp_footer_message(self, string, duration=3):
|
||||||
self.loop.remove_alarm(self.last_alarm)
|
self.loop.remove_alarm(self.last_alarm)
|
||||||
self.last_alarm = self.loop.set_alarm_in(duration, self.reset_footer)
|
self.last_alarm = self.loop.set_alarm_in(duration, self.reset_footer)
|
||||||
|
@ -1607,7 +1550,6 @@ class App(object):
|
||||||
else:
|
else:
|
||||||
self.set_footer(string)
|
self.set_footer(string)
|
||||||
|
|
||||||
|
|
||||||
def overthrow_ext_edit(self, init_body=""):
|
def overthrow_ext_edit(self, init_body=""):
|
||||||
"""
|
"""
|
||||||
Opens the external editor, but instead of integreating it into the app,
|
Opens the external editor, but instead of integreating it into the app,
|
||||||
|
@ -1625,7 +1567,6 @@ class App(object):
|
||||||
self.loop.start()
|
self.loop.start()
|
||||||
return body.strip()
|
return body.strip()
|
||||||
|
|
||||||
|
|
||||||
def compose(self, title=None, init_body="", edit=False):
|
def compose(self, title=None, init_body="", edit=False):
|
||||||
"""
|
"""
|
||||||
Dispatches the appropriate composure mode and widget based on application
|
Dispatches the appropriate composure mode and widget based on application
|
||||||
|
@ -1635,7 +1576,8 @@ 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:
|
||||||
|
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)
|
||||||
|
@ -1719,6 +1661,7 @@ class MessageBody(urwid.Text):
|
||||||
"""
|
"""
|
||||||
An urwid.Text object that works with the BBJ formatting directives.
|
An urwid.Text object that works with the BBJ formatting directives.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
if message["send_raw"]:
|
if message["send_raw"]:
|
||||||
return super(MessageBody, self).__init__(message["body"])
|
return super(MessageBody, self).__init__(message["body"])
|
||||||
|
@ -1776,7 +1719,7 @@ class MessageBody(urwid.Text):
|
||||||
color += "0"
|
color += "0"
|
||||||
else:
|
else:
|
||||||
display = "[%s]" % user["user_name"]
|
display = "[%s]" % user["user_name"]
|
||||||
except: # the quote may be garbage and refer to a nonexistant post
|
except: # the quote may be garbage and refer to a nonexistent post
|
||||||
display = ""
|
display = ""
|
||||||
result.append((color, ">>%s%s" % (body, display)))
|
result.append((color, ">>%s%s" % (body, display)))
|
||||||
|
|
||||||
|
@ -1802,6 +1745,7 @@ class KeyPrompt(urwid.Edit):
|
||||||
keybinding that is pressed. Is used to customize
|
keybinding that is pressed. Is used to customize
|
||||||
keybinds across the client.
|
keybinds across the client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, initkey, callback, *callback_args):
|
def __init__(self, initkey, callback, *callback_args):
|
||||||
super(KeyPrompt, self).__init__()
|
super(KeyPrompt, self).__init__()
|
||||||
self.set_edit_text(initkey)
|
self.set_edit_text(initkey)
|
||||||
|
@ -1823,6 +1767,7 @@ class Prompt(urwid.Edit):
|
||||||
character-wise (not word-wise) movements are
|
character-wise (not word-wise) movements are
|
||||||
implemented.
|
implemented.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
if not super(Prompt, self).keypress(size, key):
|
if not super(Prompt, self).keypress(size, key):
|
||||||
return
|
return
|
||||||
|
@ -1865,7 +1810,6 @@ class FootPrompt(Prompt):
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.args = callback_args
|
self.args = callback_args
|
||||||
|
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
super(FootPrompt, self).keypress(size, key)
|
super(FootPrompt, self).keypress(size, key)
|
||||||
if key == "enter":
|
if key == "enter":
|
||||||
|
@ -1884,7 +1828,6 @@ class StringPrompt(Prompt, urwid.Edit):
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.args = callback_args
|
self.args = callback_args
|
||||||
|
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
keyl = key.lower()
|
keyl = key.lower()
|
||||||
if key == "enter":
|
if key == "enter":
|
||||||
|
@ -1905,7 +1848,6 @@ class JumpPrompt(Prompt, urwid.IntEdit):
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.args = callback_args
|
self.args = callback_args
|
||||||
|
|
||||||
|
|
||||||
def valid_char(self, char):
|
def valid_char(self, char):
|
||||||
if not (len(char) == 1 and char in "0123456789"):
|
if not (len(char) == 1 and char in "0123456789"):
|
||||||
return False
|
return False
|
||||||
|
@ -1923,7 +1865,6 @@ class JumpPrompt(Prompt, urwid.IntEdit):
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def incr(self, direction):
|
def incr(self, direction):
|
||||||
value = self.value()
|
value = self.value()
|
||||||
if direction == "down" and value > 0:
|
if direction == "down" and value > 0:
|
||||||
|
@ -1939,7 +1880,6 @@ class JumpPrompt(Prompt, urwid.IntEdit):
|
||||||
|
|
||||||
self.set_edit_pos(len(value))
|
self.set_edit_pos(len(value))
|
||||||
|
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
keyl = key.lower()
|
keyl = key.lower()
|
||||||
if key == "enter":
|
if key == "enter":
|
||||||
|
@ -1955,7 +1895,7 @@ class JumpPrompt(Prompt, urwid.IntEdit):
|
||||||
elif keyl in ("up", "ctrl p", "p", "k"):
|
elif keyl in ("up", "ctrl p", "p", "k"):
|
||||||
self.incr("up")
|
self.incr("up")
|
||||||
|
|
||||||
else: # dont use super because we want to allow zeros in this box
|
else: # don't use super because we want to allow zeros in this box
|
||||||
urwid.Edit.keypress(self, (size[0],), key)
|
urwid.Edit.keypress(self, (size[0],), key)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1982,7 +1922,6 @@ class ExternalEditor(urwid.Terminal):
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
# def confirm_anon(self, button, value):
|
# def confirm_anon(self, button, value):
|
||||||
# app.loop.widget = app.loop.widget[0]
|
# app.loop.widget = app.loop.widget[0]
|
||||||
# if not value:
|
# if not value:
|
||||||
|
@ -1993,7 +1932,6 @@ class ExternalEditor(urwid.Terminal):
|
||||||
# app.loop.start()
|
# app.loop.start()
|
||||||
# self.exterminate(anon_confirmed=True)
|
# self.exterminate(anon_confirmed=True)
|
||||||
|
|
||||||
|
|
||||||
def exterminate(self, *_, anon_confirmed=False):
|
def exterminate(self, *_, anon_confirmed=False):
|
||||||
if app.prefs["confirm_anon"] \
|
if app.prefs["confirm_anon"] \
|
||||||
and not anon_confirmed \
|
and not anon_confirmed \
|
||||||
|
@ -2055,7 +1993,6 @@ class ExternalEditor(urwid.Terminal):
|
||||||
else:
|
else:
|
||||||
app.temp_footer_message("EMPTY POST DISCARDED")
|
app.temp_footer_message("EMPTY POST DISCARDED")
|
||||||
|
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
"""
|
"""
|
||||||
The majority of the things the parent keypress method will do is
|
The majority of the things the parent keypress method will do is
|
||||||
|
@ -2108,7 +2045,6 @@ class ExternalEditor(urwid.Terminal):
|
||||||
|
|
||||||
os.write(self.master, key.encode("utf8"))
|
os.write(self.master, key.encode("utf8"))
|
||||||
|
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""
|
"""
|
||||||
Make damn sure we scoop up after ourselves here...
|
Make damn sure we scoop up after ourselves here...
|
||||||
|
@ -2151,7 +2087,6 @@ class OptionsMenu(urwid.LineBox):
|
||||||
elif keyl == "ctrl l":
|
elif keyl == "ctrl l":
|
||||||
wipe_screen()
|
wipe_screen()
|
||||||
|
|
||||||
|
|
||||||
def mouse_event(self, size, event, button, x, y, focus):
|
def mouse_event(self, size, event, button, x, y, focus):
|
||||||
if super(OptionsMenu, self).mouse_event(size, event, button, x, y, focus):
|
if super(OptionsMenu, self).mouse_event(size, event, button, x, y, focus):
|
||||||
return
|
return
|
||||||
|
@ -2167,6 +2102,7 @@ class ActionBox(urwid.ListBox):
|
||||||
The listwalker used by all the browsing pages. Most of the application
|
The listwalker used by all the browsing pages. Most of the application
|
||||||
takes place in an instance of this box. Handles many keybinds.
|
takes place in an instance of this box. Handles many keybinds.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
super(ActionBox, self).keypress(size, key)
|
super(ActionBox, self).keypress(size, key)
|
||||||
overlay = app.overlay_p()
|
overlay = app.overlay_p()
|
||||||
|
@ -2252,8 +2188,10 @@ class ActionBox(urwid.ListBox):
|
||||||
elif key == "~":
|
elif key == "~":
|
||||||
# sssssshhhhhhhh
|
# sssssshhhhhhhh
|
||||||
app.loop.stop()
|
app.loop.stop()
|
||||||
try: call("sl", shell=True)
|
try:
|
||||||
except: pass
|
call("sl", shell=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
app.loop.start()
|
app.loop.start()
|
||||||
|
|
||||||
elif keyl == "$":
|
elif keyl == "$":
|
||||||
|
@ -2273,7 +2211,6 @@ class ActionBox(urwid.ListBox):
|
||||||
elif keyl == "ctrl r":
|
elif keyl == "ctrl r":
|
||||||
app.reply(None, message)
|
app.reply(None, message)
|
||||||
|
|
||||||
|
|
||||||
def mouse_event(self, size, event, button, x, y, focus):
|
def mouse_event(self, size, event, button, x, y, focus):
|
||||||
if super(ActionBox, self).mouse_event(size, event, button, x, y, focus):
|
if super(ActionBox, self).mouse_event(size, event, button, x, y, focus):
|
||||||
return
|
return
|
||||||
|
@ -2286,7 +2223,6 @@ class ActionBox(urwid.ListBox):
|
||||||
self._keypress_down(size)
|
self._keypress_down(size)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def frilly_exit():
|
def frilly_exit():
|
||||||
"""
|
"""
|
||||||
Exit with some flair. Will fill the screen with rainbows
|
Exit with some flair. Will fill the screen with rainbows
|
||||||
|
@ -2294,8 +2230,10 @@ def frilly_exit():
|
||||||
setting, `dramatic_exit`
|
setting, `dramatic_exit`
|
||||||
"""
|
"""
|
||||||
# sometimes this gets called before the loop is set up properly
|
# sometimes this gets called before the loop is set up properly
|
||||||
try: app.loop.stop()
|
try:
|
||||||
except: pass
|
app.loop.stop()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
if app.prefs["dramatic_exit"] and app.loop.screen_size:
|
if app.prefs["dramatic_exit"] and app.loop.screen_size:
|
||||||
width, height = app.loop.screen_size
|
width, height = app.loop.screen_size
|
||||||
for x in range(height - 1):
|
for x in range(height - 1):
|
||||||
|
@ -2328,7 +2266,8 @@ def urwid_rainbows(string, bold=False):
|
||||||
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]
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
@ -2344,7 +2283,7 @@ def motherfucking_rainbows(string, inputmode=False, end="\n"):
|
||||||
return print(end, end="")
|
return print(end, end="")
|
||||||
|
|
||||||
|
|
||||||
def paren_prompt(text, positive=True, choices=[], function=input, default=None):
|
def paren_prompt(text, positive=True, choices=None, function=input, default=None):
|
||||||
"""
|
"""
|
||||||
input(), but riced the fuck out. Changes color depending on
|
input(), but riced the fuck out. Changes color depending on
|
||||||
the value of positive (blue/green for good stuff, red/yellow
|
the value of positive (blue/green for good stuff, red/yellow
|
||||||
|
@ -2352,6 +2291,8 @@ def paren_prompt(text, positive=True, choices=[], function=input, default=None):
|
||||||
system capable of rejecting unavailable choices and highlighting
|
system capable of rejecting unavailable choices and highlighting
|
||||||
their first characters.
|
their first characters.
|
||||||
"""
|
"""
|
||||||
|
if choices is None:
|
||||||
|
choices = []
|
||||||
end = text[-1]
|
end = text[-1]
|
||||||
if end != "?" and end in punctuation:
|
if end != "?" and end in punctuation:
|
||||||
text = text[0:-1]
|
text = text[0:-1]
|
||||||
|
@ -2389,7 +2330,8 @@ def sane_value(key, prompt, positive=True, return_empty=False):
|
||||||
response = paren_prompt(prompt, positive)
|
response = paren_prompt(prompt, positive)
|
||||||
if return_empty and response == "":
|
if return_empty and response == "":
|
||||||
return response
|
return response
|
||||||
try: network.validate(key, response)
|
try:
|
||||||
|
network.validate(key, response)
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
return sane_value(key, e.description, False)
|
return sane_value(key, e.description, False)
|
||||||
return response
|
return response
|
||||||
|
@ -2488,7 +2430,7 @@ def bbjrc(mode, **params):
|
||||||
# Also covers a previous encounter a user
|
# Also covers a previous encounter a user
|
||||||
# had with having a NoneType set in their
|
# had with having a NoneType set in their
|
||||||
# config by accident, crashing the program.
|
# config by accident, crashing the program.
|
||||||
if key not in values or values[key] == None:
|
if key not in values or values[key] is None:
|
||||||
values[key] = default_value
|
values[key] = default_value
|
||||||
# else make one
|
# else make one
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
@ -2505,7 +2447,7 @@ def bbjrc(mode, **params):
|
||||||
def mark(directive=True):
|
def mark(directive=True):
|
||||||
"""
|
"""
|
||||||
Set and retrieve positional marks for threads.
|
Set and retrieve positional marks for threads.
|
||||||
This uses a seperate file from the preferences
|
This uses a separate file from the preferences
|
||||||
to keep it free from clutter.
|
to keep it free from clutter.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
@ -2514,7 +2456,7 @@ def mark(directive=True):
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
values = {}
|
values = {}
|
||||||
|
|
||||||
if directive == True and app.mode == "thread":
|
if directive and app.mode == "thread":
|
||||||
pos = app.get_focus_post()
|
pos = app.get_focus_post()
|
||||||
values[app.thread["thread_id"]] = pos
|
values[app.thread["thread_id"]] = pos
|
||||||
with open(markpath, "w") as _out:
|
with open(markpath, "w") as _out:
|
||||||
|
@ -2564,8 +2506,8 @@ def ignore(*_, **__):
|
||||||
|
|
||||||
def wipe_screen(*_):
|
def wipe_screen(*_):
|
||||||
"""
|
"""
|
||||||
A crude hack to repaint the whole screen. I didnt immediately
|
A crude hack to repaint the whole screen. I didn't immediately
|
||||||
see anything to acheive this in the MainLoop methods so this
|
see anything to achieve this in the MainLoop methods so this
|
||||||
will do, I suppose.
|
will do, I suppose.
|
||||||
"""
|
"""
|
||||||
app.loop.stop()
|
app.loop.stop()
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
[Unit]
|
||||||
|
Description=bbj daemon
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/srv/bbj/bbj
|
||||||
|
ExecStart=/srv/bbj/bbj/venv/bin/python3 server.py
|
||||||
|
User=bbj
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StartLimitInterval=60s
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec /srv/bbj/venv/bin/python3 /srv/bbj/clients/urwid/main.py
|
|
@ -12,7 +12,7 @@ at the root:
|
||||||
|
|
||||||
`http://server.com/api/endpoint_here`
|
`http://server.com/api/endpoint_here`
|
||||||
|
|
||||||
The body of your request contains all of it's argument fields, instead of
|
The body of your request contains all of its argument fields, instead of
|
||||||
using URL parameters. As a demonstration, to call `thread_create`,
|
using URL parameters. As a demonstration, to call `thread_create`,
|
||||||
it requires two arguments: `title`, and `body`. We put those argument
|
it requires two arguments: `title`, and `body`. We put those argument
|
||||||
names at the root of the json object, and their values are the info
|
names at the root of the json object, and their values are the info
|
||||||
|
@ -33,13 +33,13 @@ GET these if you so choose.
|
||||||
|
|
||||||
For all endpoints, argument keys that are not consumed by the endpoint are
|
For all endpoints, argument keys that are not consumed by the endpoint are
|
||||||
ignored. Posting an object with a key/value pair of `"sandwich": True` will
|
ignored. Posting an object with a key/value pair of `"sandwich": True` will
|
||||||
not clog up any pipes :) In the same vein, endpoints who dont take arguments
|
not clog up any pipes :) In the same vein, endpoints who don't take arguments
|
||||||
don't care if you supply them anyway.
|
don't care if you supply them anyway.
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
|
||||||
BBJ returns data in a consistently formatted json object. The base object
|
BBJ returns data in a consistently formatted json object. The base object
|
||||||
has three keys: `data`, `usermap`, and `error`. Visualizied:
|
has three keys: `data`, `usermap`, and `error`. Visualized:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
|
@ -74,7 +74,7 @@ objects. BBJ handles users entirely by an ID system, meaning any references
|
||||||
to them inside of response data will not include vital information like their
|
to them inside of response data will not include vital information like their
|
||||||
username, or their profile information. Instead, we fetch those values from
|
username, or their profile information. Instead, we fetch those values from
|
||||||
this usermap object. All of it's root keys are user_id's and their values
|
this usermap object. All of it's root keys are user_id's and their values
|
||||||
are user objects. It should be noted that the anonymous user has it's own
|
are user objects. It should be noted that the anonymous user has its own
|
||||||
ID and profile object as well.
|
ID and profile object as well.
|
||||||
|
|
||||||
### error
|
### error
|
||||||
|
@ -121,7 +121,7 @@ is correct for the given user.
|
||||||
Requires the arguments `thread_id` and `post_id`.
|
Requires the arguments `thread_id` and `post_id`.
|
||||||
|
|
||||||
Delete a message from a thread. The same rules apply
|
Delete a message from a thread. The same rules apply
|
||||||
here as `edit_post` and `edit_query`: the logged in user
|
here as `edit_post` and `edit_query`: the logged-in user
|
||||||
must either be the one who posted the message within 24hrs,
|
must either be the one who posted the message within 24hrs,
|
||||||
or have admin rights. The same error descriptions and code
|
or have admin rights. The same error descriptions and code
|
||||||
are returned on falilure. Boolean true is returned on
|
are returned on falilure. Boolean true is returned on
|
||||||
|
@ -215,7 +215,7 @@ you can access metadata for these threads by the `threads` object
|
||||||
which is also provided.
|
which is also provided.
|
||||||
|
|
||||||
The `messages` array is already sorted by submission time, newest
|
The `messages` array is already sorted by submission time, newest
|
||||||
first. The order in the threads object is undefined and you should
|
first. The order in the threads object is undefined, and you should
|
||||||
instead use their `last_mod` attribute if you intend to list them
|
instead use their `last_mod` attribute if you intend to list them
|
||||||
out visually.
|
out visually.
|
||||||
|
|
||||||
|
@ -319,7 +319,7 @@ for the original post.
|
||||||
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`. `format` may also be
|
Requires the argument `thread_id`. `format` may also be
|
||||||
specified as a formatter to run the messages through.
|
specified as a formatter to run the messages through.
|
||||||
Currently only "sequential" is supported.
|
Currently, only "sequential" is supported.
|
||||||
|
|
||||||
You may also supply the parameter `op_only`. When it's value
|
You may also supply the parameter `op_only`. When it's value
|
||||||
is non-nil, the messages array will only include post_id 0 (the first)
|
is non-nil, the messages array will only include post_id 0 (the first)
|
||||||
|
|
|
@ -23,7 +23,7 @@ users.
|
||||||
|
|
||||||
* **Code 0**: Malformed but non-empty json input. An empty json input where it is required is handled by code 3. This is just decoding errors. The exception text is returned as description.
|
* **Code 0**: Malformed but non-empty json input. An empty json input where it is required is handled by code 3. This is just decoding errors. The exception text is returned as description.
|
||||||
|
|
||||||
* **Code 1**: Internal server error. A short representation of the internal exception as well as the code the server logged it as is returned in the description. Your clients cannot recover from this class of error, and its probably not your fault if you encounter it. If you ever get one, file a bug report.
|
* **Code 1**: Internal server error. A short representation of the internal exception as well as the code the server logged it as is returned in the description. Your clients cannot recover from this class of error, and it's probably not your fault if you encounter it. If you ever get one, file a bug report.
|
||||||
|
|
||||||
* **Code 2**: Server HTTP error: This is similar to the above but captures errors for the HTTP server rather than BBJs own codebase. The description contains the HTTP error code and server description. This notably covers 404s and thus invalid endpoint names. The HTTP error code is left intact, so you may choose to let your HTTP library or tool of choice handle these for you.
|
* **Code 2**: Server HTTP error: This is similar to the above but captures errors for the HTTP server rather than BBJs own codebase. The description contains the HTTP error code and server description. This notably covers 404s and thus invalid endpoint names. The HTTP error code is left intact, so you may choose to let your HTTP library or tool of choice handle these for you.
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ users.</p>
|
||||||
<p><strong>Code 0</strong>: Malformed but non-empty json input. An empty json input where it is required is handled by code 3. This is just decoding errors. The exception text is returned as description.</p>
|
<p><strong>Code 0</strong>: Malformed but non-empty json input. An empty json input where it is required is handled by code 3. This is just decoding errors. The exception text is returned as description.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p><strong>Code 1</strong>: Internal server error. A short representation of the internal exception as well as the code the server logged it as is returned in the description. Your clients cannot recover from this class of error, and its probably not your fault if you encounter it. If you ever get one, file a bug report.</p>
|
<p><strong>Code 1</strong>: Internal server error. A short representation of the internal exception as well as the code the server logged it as is returned in the description. Your clients cannot recover from this class of error, and It's probably not your fault if you encounter it. If you ever get one, file a bug report.</p>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<p><strong>Code 2</strong>: Server HTTP error: This is similar to the above but captures errors for the HTTP server rather than BBJs own codebase. The description contains the HTTP error code and server description. This notably covers 404s and thus invalid endpoint names. The HTTP error code is left intact, so you may choose to let your HTTP library or tool of choice handle these for you.</p>
|
<p><strong>Code 2</strong>: Server HTTP error: This is similar to the above but captures errors for the HTTP server rather than BBJs own codebase. The description contains the HTTP error code and server description. This notably covers 404s and thus invalid endpoint names. The HTTP error code is left intact, so you may choose to let your HTTP library or tool of choice handle these for you.</p>
|
||||||
|
|
|
@ -107,7 +107,7 @@ attributes like a more traditional forum. Registration is optional and there
|
||||||
are only minimal restrictions on anonymous participation.</p>
|
are only minimal restrictions on anonymous participation.</p>
|
||||||
<p><img alt="screenshot" src="./img/screenshot.png" /></p>
|
<p><img alt="screenshot" src="./img/screenshot.png" /></p>
|
||||||
<p>Being a command-line-oriented text board, BBJ has no avatars or file sharing
|
<p>Being a command-line-oriented text board, BBJ has no avatars or file sharing
|
||||||
capabilties, so its easier to administrate and can't be used to distribute illegal
|
capabilties, so it's easier to administrate and can't be used to distribute illegal
|
||||||
content like imageboards. It has very few dependancies and is easy to set up.</p>
|
content like imageboards. It has very few dependancies and is easy to set up.</p>
|
||||||
<p>The API is simple and doesn't use require complex authorization schemes or session management.
|
<p>The API is simple and doesn't use require complex authorization schemes or session management.
|
||||||
It is fully documented on this site (though the verbage is still being revised for friendliness)</p></div>
|
It is fully documented on this site (though the verbage is still being revised for friendliness)</p></div>
|
||||||
|
|
|
@ -13,10 +13,10 @@ define(['module'], function (module) {
|
||||||
|
|
||||||
var text, fs, Cc, Ci, xpcIsWindows,
|
var text, fs, Cc, Ci, xpcIsWindows,
|
||||||
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
|
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
|
||||||
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
|
xmlRegExp = /^\s*<\?xml(\s)+version=['"](\d)*.(\d)*['"](\s)*\?>/im,
|
||||||
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
|
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
|
||||||
hasLocation = typeof location !== 'undefined' && location.href,
|
hasLocation = typeof location !== 'undefined' && location.href,
|
||||||
defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
|
defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/:/, ''),
|
||||||
defaultHostName = hasLocation && location.hostname,
|
defaultHostName = hasLocation && location.hostname,
|
||||||
defaultPort = hasLocation && (location.port || undefined),
|
defaultPort = hasLocation && (location.port || undefined),
|
||||||
buildMap = {},
|
buildMap = {},
|
||||||
|
@ -116,7 +116,7 @@ define(['module'], function (module) {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
|
xdRegExp: /^((\w+):)?\/\/([^\/\\]+)/,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is an URL on another domain. Only works for browser use, returns
|
* Is an URL on another domain. Only works for browser use, returns
|
||||||
|
|
|
@ -4,6 +4,4 @@
|
||||||
# Nothing magical here.
|
# Nothing magical here.
|
||||||
|
|
||||||
python3 ./mkendpoints.py
|
python3 ./mkendpoints.py
|
||||||
cd ./docs
|
(cd docs; mkdocs build)
|
||||||
mkdocs build
|
|
||||||
cd ..
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ def geterr(obj):
|
||||||
error = obj.get("error")
|
error = obj.get("error")
|
||||||
if not error:
|
if not error:
|
||||||
return False
|
return False
|
||||||
return (error["code"], error["description"])
|
return error["code"], error["description"]
|
||||||
|
|
||||||
|
|
||||||
def register_prompt(user, initial=True):
|
def register_prompt(user, initial=True):
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from src import db
|
||||||
from src import formatting
|
from src import formatting
|
||||||
from src import schema
|
from src import schema
|
||||||
from time import time
|
|
||||||
from src import db
|
|
||||||
|
|
||||||
|
|
||||||
endpoints = {
|
endpoints = {
|
||||||
"check_auth": ["user", "auth_hash"],
|
"check_auth": ["user", "auth_hash"],
|
||||||
|
@ -20,7 +20,6 @@ endpoints = {
|
||||||
"user_name_to_id": ["target_user"]
|
"user_name_to_id": ["target_user"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
authless = [
|
authless = [
|
||||||
"is_registered",
|
"is_registered",
|
||||||
"user_register"
|
"user_register"
|
||||||
|
@ -58,7 +57,7 @@ def is_registered(json):
|
||||||
|
|
||||||
|
|
||||||
def check_auth(json):
|
def check_auth(json):
|
||||||
"Returns true or false whether auth_hashes matches user."
|
"""Returns true or false whether auth_hashes matches user."""
|
||||||
return bool(db.user_auth(json["user"], json["auth_hash"]))
|
return bool(db.user_auth(json["user"], json["auth_hash"]))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
CherryPy~=18.6.1
|
||||||
|
Markdown~=3.3.6
|
||||||
|
urwid~=2.1.2
|
42
server.py
42
server.py
|
@ -1,16 +1,18 @@
|
||||||
from src.exceptions import BBJException, BBJParameterError, BBJUserError
|
|
||||||
from src import db, schema, formatting
|
|
||||||
from functools import wraps
|
|
||||||
from uuid import uuid1
|
|
||||||
from sys import argv
|
|
||||||
import traceback
|
|
||||||
import cherrypy
|
|
||||||
import sqlite3
|
|
||||||
import json
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import traceback
|
||||||
|
from functools import wraps
|
||||||
|
from sys import argv
|
||||||
|
from uuid import uuid1
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
from src import db, schema, formatting
|
||||||
|
from src.exceptions import BBJException, BBJParameterError, BBJUserError
|
||||||
|
|
||||||
dbname = "data.sqlite"
|
dbname = "data.sqlite"
|
||||||
|
|
||||||
# any values here may be overrided in the config.json. Any values not listed
|
# any values here may be overridden in the config.json. Any values not listed
|
||||||
# here will have no effect on the server.
|
# here will have no effect on the server.
|
||||||
default_config = {
|
default_config = {
|
||||||
"admins": [],
|
"admins": [],
|
||||||
|
@ -29,7 +31,7 @@ try:
|
||||||
# The application will never store a config value
|
# The application will never store a config value
|
||||||
# as the NoneType, so users may set an option as
|
# as the NoneType, so users may set an option as
|
||||||
# null in their file to reset it to default
|
# null in their file to reset it to default
|
||||||
if key not in app_config or app_config[key] == None:
|
if key not in app_config or app_config[key] is None:
|
||||||
app_config[key] = default_value
|
app_config[key] = default_value
|
||||||
# else just use the defaults
|
# else just use the defaults
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
@ -217,6 +219,7 @@ class API(object):
|
||||||
validate(args, ["user_name", "auth_hash"])
|
validate(args, ["user_name", "auth_hash"])
|
||||||
return db.user_register(
|
return db.user_register(
|
||||||
database, args["user_name"], args["auth_hash"])
|
database, args["user_name"], args["auth_hash"])
|
||||||
|
|
||||||
user_register.doctype = "Users"
|
user_register.doctype = "Users"
|
||||||
user_register.arglist = (
|
user_register.arglist = (
|
||||||
("user_name", "string: the desired display name"),
|
("user_name", "string: the desired display name"),
|
||||||
|
@ -240,6 +243,7 @@ class API(object):
|
||||||
no_anon_hook(user, "Anons cannot modify their account.")
|
no_anon_hook(user, "Anons cannot modify their account.")
|
||||||
validate(args, []) # just make sure its not empty
|
validate(args, []) # just make sure its not empty
|
||||||
return db.user_update(database, user, args)
|
return db.user_update(database, user, args)
|
||||||
|
|
||||||
user_update.doctype = "Users"
|
user_update.doctype = "Users"
|
||||||
user_update.arglist = (
|
user_update.arglist = (
|
||||||
("Any of the following may be submitted", ""),
|
("Any of the following may be submitted", ""),
|
||||||
|
@ -257,6 +261,7 @@ class API(object):
|
||||||
including your `auth_hash`.
|
including your `auth_hash`.
|
||||||
"""
|
"""
|
||||||
return user
|
return user
|
||||||
|
|
||||||
get_me.doctype = "Users"
|
get_me.doctype = "Users"
|
||||||
get_me.arglist = (("", ""),)
|
get_me.arglist = (("", ""),)
|
||||||
|
|
||||||
|
@ -281,6 +286,7 @@ class API(object):
|
||||||
for user in users
|
for user in users
|
||||||
}
|
}
|
||||||
return list(users)
|
return list(users)
|
||||||
|
|
||||||
user_map.doctype = "Tools"
|
user_map.doctype = "Tools"
|
||||||
user_map.arglist = (("", ""),)
|
user_map.arglist = (("", ""),)
|
||||||
|
|
||||||
|
@ -292,6 +298,7 @@ class API(object):
|
||||||
validate(args, ["target_user"])
|
validate(args, ["target_user"])
|
||||||
return db.user_resolve(
|
return db.user_resolve(
|
||||||
database, args["target_user"], return_false=False, externalize=True)
|
database, args["target_user"], return_false=False, externalize=True)
|
||||||
|
|
||||||
user_get.doctype = "Users"
|
user_get.doctype = "Users"
|
||||||
user_get.arglist = (
|
user_get.arglist = (
|
||||||
("target_user", "string: either a user_name or a user_id"),
|
("target_user", "string: either a user_name or a user_id"),
|
||||||
|
@ -305,6 +312,7 @@ class API(object):
|
||||||
"""
|
"""
|
||||||
validate(args, ["target_user"])
|
validate(args, ["target_user"])
|
||||||
return bool(db.user_resolve(database, args["target_user"]))
|
return bool(db.user_resolve(database, args["target_user"]))
|
||||||
|
|
||||||
user_is_registered.doctype = "Users"
|
user_is_registered.doctype = "Users"
|
||||||
user_is_registered.arglist = (
|
user_is_registered.arglist = (
|
||||||
("target_user", "string: either a user_name or a user_id"),
|
("target_user", "string: either a user_name or a user_id"),
|
||||||
|
@ -320,6 +328,7 @@ class API(object):
|
||||||
user = db.user_resolve(
|
user = db.user_resolve(
|
||||||
database, args["target_user"], return_false=False)
|
database, args["target_user"], return_false=False)
|
||||||
return args["target_hash"].lower() == user["auth_hash"].lower()
|
return args["target_hash"].lower() == user["auth_hash"].lower()
|
||||||
|
|
||||||
check_auth.doctype = "Authorization"
|
check_auth.doctype = "Authorization"
|
||||||
check_auth.arglist = (
|
check_auth.arglist = (
|
||||||
("target_user", "string: either a user_name or a user_id"),
|
("target_user", "string: either a user_name or a user_id"),
|
||||||
|
@ -338,6 +347,7 @@ class API(object):
|
||||||
threads = db.thread_index(database, include_op=args.get("include_op"))
|
threads = db.thread_index(database, include_op=args.get("include_op"))
|
||||||
cherrypy.thread_data.usermap = create_usermap(database, threads, True)
|
cherrypy.thread_data.usermap = create_usermap(database, threads, True)
|
||||||
return threads
|
return threads
|
||||||
|
|
||||||
thread_index.doctype = "Threads & Messages"
|
thread_index.doctype = "Threads & Messages"
|
||||||
thread_index.arglist = (
|
thread_index.arglist = (
|
||||||
("OPTIONAL: include_op", "boolean: Include a `messages` object containing the original post"),
|
("OPTIONAL: include_op", "boolean: Include a `messages` object containing the original post"),
|
||||||
|
@ -382,6 +392,7 @@ class API(object):
|
||||||
|
|
||||||
do_formatting(args.get("format"), feed["messages"])
|
do_formatting(args.get("format"), feed["messages"])
|
||||||
return feed
|
return feed
|
||||||
|
|
||||||
message_feed.doctype = "Threads & Messages"
|
message_feed.doctype = "Threads & Messages"
|
||||||
message_feed.arglist = (
|
message_feed.arglist = (
|
||||||
("time", "int/float: epoch/unix time of the earliest point of interest"),
|
("time", "int/float: epoch/unix time of the earliest point of interest"),
|
||||||
|
@ -405,6 +416,7 @@ class API(object):
|
||||||
cherrypy.thread_data.usermap = \
|
cherrypy.thread_data.usermap = \
|
||||||
create_usermap(database, thread["messages"])
|
create_usermap(database, thread["messages"])
|
||||||
return thread
|
return thread
|
||||||
|
|
||||||
thread_create.doctype = "Threads & Messages"
|
thread_create.doctype = "Threads & Messages"
|
||||||
thread_create.arglist = (
|
thread_create.arglist = (
|
||||||
("body", "string: The body of the first message"),
|
("body", "string: The body of the first message"),
|
||||||
|
@ -426,6 +438,7 @@ class API(object):
|
||||||
return db.thread_reply(
|
return db.thread_reply(
|
||||||
database, user["user_id"], args["thread_id"],
|
database, user["user_id"], args["thread_id"],
|
||||||
args["body"], args.get("send_raw"))
|
args["body"], args.get("send_raw"))
|
||||||
|
|
||||||
thread_reply.doctype = "Threads & Messages"
|
thread_reply.doctype = "Threads & Messages"
|
||||||
thread_reply.arglist = (
|
thread_reply.arglist = (
|
||||||
("thread_id", "string: the id for the thread this message should post to."),
|
("thread_id", "string: the id for the thread this message should post to."),
|
||||||
|
@ -451,6 +464,7 @@ class API(object):
|
||||||
create_usermap(database, thread["messages"])
|
create_usermap(database, thread["messages"])
|
||||||
do_formatting(args.get("format"), thread["messages"])
|
do_formatting(args.get("format"), thread["messages"])
|
||||||
return thread
|
return thread
|
||||||
|
|
||||||
thread_load.doctype = "Threads & Messages"
|
thread_load.doctype = "Threads & Messages"
|
||||||
thread_load.arglist = (
|
thread_load.arglist = (
|
||||||
("thread_id", "string: the thread to load."),
|
("thread_id", "string: the thread to load."),
|
||||||
|
@ -484,6 +498,7 @@ class API(object):
|
||||||
return db.message_edit_commit(
|
return db.message_edit_commit(
|
||||||
database, user["user_id"], args["thread_id"],
|
database, user["user_id"], args["thread_id"],
|
||||||
args["post_id"], args["body"], args.get("send_raw"))
|
args["post_id"], args["body"], args.get("send_raw"))
|
||||||
|
|
||||||
edit_post.doctype = "Threads & Messages"
|
edit_post.doctype = "Threads & Messages"
|
||||||
edit_post.arglist = (
|
edit_post.arglist = (
|
||||||
("thread_id", "string: the thread the message was posted in."),
|
("thread_id", "string: the thread the message was posted in."),
|
||||||
|
@ -510,6 +525,7 @@ class API(object):
|
||||||
validate(args, ["thread_id", "post_id"])
|
validate(args, ["thread_id", "post_id"])
|
||||||
return db.message_delete(
|
return db.message_delete(
|
||||||
database, user["user_id"], args["thread_id"], args["post_id"])
|
database, user["user_id"], args["thread_id"], args["post_id"])
|
||||||
|
|
||||||
delete_post.doctype = "Threads & Messages"
|
delete_post.doctype = "Threads & Messages"
|
||||||
delete_post.arglist = (
|
delete_post.arglist = (
|
||||||
("thread_id", "string: the id of the thread this message was posted in."),
|
("thread_id", "string: the id of the thread this message was posted in."),
|
||||||
|
@ -539,6 +555,7 @@ class API(object):
|
||||||
database, user["user_id"],
|
database, user["user_id"],
|
||||||
args["thread_id"], args["post_id"],
|
args["thread_id"], args["post_id"],
|
||||||
None, args["value"], None)
|
None, args["value"], None)
|
||||||
|
|
||||||
set_post_raw.doctype = "Threads & Messages"
|
set_post_raw.doctype = "Threads & Messages"
|
||||||
set_post_raw.arglist = (
|
set_post_raw.arglist = (
|
||||||
("thread_id", "string: the id of the thread the message was posted in."),
|
("thread_id", "string: the id of the thread the message was posted in."),
|
||||||
|
@ -556,6 +573,7 @@ class API(object):
|
||||||
user = db.user_resolve(
|
user = db.user_resolve(
|
||||||
database, args["target_user"], return_false=False)
|
database, args["target_user"], return_false=False)
|
||||||
return user["is_admin"]
|
return user["is_admin"]
|
||||||
|
|
||||||
is_admin.doctype = "Users"
|
is_admin.doctype = "Users"
|
||||||
is_admin.arglist = (
|
is_admin.arglist = (
|
||||||
("target_user", "string: user_id or user_name to check against."),
|
("target_user", "string: user_id or user_name to check against."),
|
||||||
|
@ -575,6 +593,7 @@ class API(object):
|
||||||
validate(args, ["thread_id", "post_id"])
|
validate(args, ["thread_id", "post_id"])
|
||||||
return db.message_edit_query(
|
return db.message_edit_query(
|
||||||
database, user["user_id"], args["thread_id"], args["post_id"])
|
database, user["user_id"], args["thread_id"], args["post_id"])
|
||||||
|
|
||||||
edit_query.doctype = "Threads & Messages"
|
edit_query.doctype = "Threads & Messages"
|
||||||
edit_query.arglist = (
|
edit_query.arglist = (
|
||||||
("thread_id", "string: the id of the thread the message was posted in."),
|
("thread_id", "string: the id of the thread the message was posted in."),
|
||||||
|
@ -592,6 +611,7 @@ class API(object):
|
||||||
message = [{"body": args["body"]}]
|
message = [{"body": args["body"]}]
|
||||||
do_formatting(args["format"], message)
|
do_formatting(args["format"], message)
|
||||||
return message[0]["body"]
|
return message[0]["body"]
|
||||||
|
|
||||||
format_message.doctype = "Tools"
|
format_message.doctype = "Tools"
|
||||||
format_message.arglist = (
|
format_message.arglist = (
|
||||||
("body", "string: the message body to apply formatting to."),
|
("body", "string: the message body to apply formatting to."),
|
||||||
|
@ -613,6 +633,7 @@ class API(object):
|
||||||
if not user["is_admin"]:
|
if not user["is_admin"]:
|
||||||
raise BBJUserError("Only admins can set thread pins")
|
raise BBJUserError("Only admins can set thread pins")
|
||||||
return db.thread_set_pin(database, args["thread_id"], args["value"])
|
return db.thread_set_pin(database, args["thread_id"], args["value"])
|
||||||
|
|
||||||
thread_set_pin.doctype = "Threads & Messages"
|
thread_set_pin.doctype = "Threads & Messages"
|
||||||
thread_set_pin.arglist = (
|
thread_set_pin.arglist = (
|
||||||
("thread_id", "string: the id of the thread to modify."),
|
("thread_id", "string: the id of the thread to modify."),
|
||||||
|
@ -656,6 +677,7 @@ class API(object):
|
||||||
response["bool"] = False
|
response["bool"] = False
|
||||||
response["description"] = e.description
|
response["description"] = e.description
|
||||||
return response
|
return response
|
||||||
|
|
||||||
db_validate.doctype = "Tools"
|
db_validate.doctype = "Tools"
|
||||||
db_validate.arglist = (
|
db_validate.arglist = (
|
||||||
("key", "string: the identifier for the ruleset to check."),
|
("key", "string: the identifier for the ruleset to check."),
|
||||||
|
|
34
setup.sh
34
setup.sh
|
@ -1,9 +1,9 @@
|
||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
|
|
||||||
DEPS=(
|
create_db() {
|
||||||
cherrypy
|
sqlite3 data.sqlite < schema.sql
|
||||||
urwid
|
chmod 600 data.sqlite
|
||||||
)
|
}
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
--help)
|
--help)
|
||||||
|
@ -20,24 +20,20 @@ EOF
|
||||||
exit;;
|
exit;;
|
||||||
|
|
||||||
--dbset)
|
--dbset)
|
||||||
sqlite3 data.sqlite < schema.sql
|
create_db
|
||||||
echo cleared
|
|
||||||
chmod 600 data.sqlite
|
|
||||||
exit;;
|
exit;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
[[ -e logs ]] || mkdir logs; mkdir logs/exceptions
|
[ -e logs ] || mkdir -p logs/exceptions
|
||||||
|
|
||||||
PYTHON=`which python3`
|
PYTHON=$(which python3)
|
||||||
[[ -z $1 ]] || PYTHON=$1
|
[ -z "$1" ] || PYTHON="$1"
|
||||||
echo Using $PYTHON...
|
printf "Using %s...\n" "$PYTHON"
|
||||||
$PYTHON -m pip install ${DEPS[*]}
|
$PYTHON -m pip install -r requirements.txt
|
||||||
|
|
||||||
echo "Enter [i] to initialize a new database"
|
printf "Enter [i] to initialize a new database\n"
|
||||||
read CLEAR
|
read -r CLEAR
|
||||||
|
|
||||||
if [[ $CLEAR == "i" ]]; then
|
if [ "$CLEAR" = "i" ]; then
|
||||||
sqlite3 data.sqlite < schema.sql
|
create_db
|
||||||
chmod 600 data.sqlite
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
28
src/db.py
28
src/db.py
|
@ -1,12 +1,12 @@
|
||||||
"""
|
"""
|
||||||
This module contains all of the interaction with the SQLite database. It
|
This module contains all the interaction with the SQLite database. It
|
||||||
doesnt hold a connection itself, rather, a connection is passed in as
|
doesn't hold a connection itself, rather, a connection is passed in as
|
||||||
an argument to all the functions and is maintained by CherryPy's threading
|
an argument to all the functions and is maintained by CherryPy's threading
|
||||||
system. This is clunky but fuck it, it works (for now at least).
|
system. This is clunky but fuck it, it works (for now at least).
|
||||||
|
|
||||||
All post and thread data are stored in the database without formatting.
|
All post and thread data are stored in the database without formatting.
|
||||||
This is questionable, as it causes formatting to be reapplied with each
|
This is questionable, as it causes formatting to be reapplied with each
|
||||||
pull for the database. Im debating whether posts should be stored in all
|
pull for the database. I'm debating whether posts should be stored in all
|
||||||
4 formats, or if maybe a caching system should be used.
|
4 formats, or if maybe a caching system should be used.
|
||||||
|
|
||||||
The database, nor ANY part of the server, DOES NOT HANDLE PASSWORD HASHING!
|
The database, nor ANY part of the server, DOES NOT HANDLE PASSWORD HASHING!
|
||||||
|
@ -20,13 +20,12 @@ use of sha256.
|
||||||
# database user object: these user objects are always resolved on
|
# database user object: these user objects are always resolved on
|
||||||
# incoming requests and re-resolving them from their ID is wasteful.
|
# incoming requests and re-resolving them from their ID is wasteful.
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
from uuid import uuid1
|
||||||
|
|
||||||
|
from src import schema
|
||||||
from src.exceptions import BBJParameterError, BBJUserError
|
from src.exceptions import BBJParameterError, BBJUserError
|
||||||
from src.utils import ordered_keys, schema_values
|
from src.utils import ordered_keys, schema_values
|
||||||
from src import schema
|
|
||||||
from uuid import uuid1
|
|
||||||
from time import time
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
anon = None
|
anon = None
|
||||||
|
|
||||||
|
@ -97,10 +96,9 @@ def thread_get(connection, thread_id, messages=True, op_only=False):
|
||||||
thread = schema.thread(*thread)
|
thread = schema.thread(*thread)
|
||||||
|
|
||||||
if messages or op_only:
|
if messages or op_only:
|
||||||
query = "SELECT * FROM messages WHERE thread_id = ? %s"
|
query = "SELECT * FROM messages WHERE thread_id = ? "
|
||||||
c.execute(query % (
|
query += "AND post_id = 0" if op_only else "ORDER BY post_id"
|
||||||
"AND post_id = 0" if op_only else "ORDER BY post_id"
|
c.execute(query, (thread_id,))
|
||||||
), (thread_id,))
|
|
||||||
# create a list where each post_id matches its list[index]
|
# create a list where each post_id matches its list[index]
|
||||||
thread["messages"] = [schema.message(*values) for values in c.fetchall()]
|
thread["messages"] = [schema.message(*values) for values in c.fetchall()]
|
||||||
|
|
||||||
|
@ -230,7 +228,7 @@ def message_delete(connection, author, thread_id, post_id):
|
||||||
WHERE thread_id = ?
|
WHERE thread_id = ?
|
||||||
AND post_id = ?
|
AND post_id = ?
|
||||||
""", (anon["user_id"], "[deleted]", False, thread_id, post_id))
|
""", (anon["user_id"], "[deleted]", False, thread_id, post_id))
|
||||||
# DONT deincrement the reply_count of this thread,
|
# DON'T decrement the reply_count of this thread,
|
||||||
# or even delete the message itself. This breaks
|
# or even delete the message itself. This breaks
|
||||||
# balance between post_id and the post's index when
|
# balance between post_id and the post's index when
|
||||||
# the thread is served with the messages in an array.
|
# the thread is served with the messages in an array.
|
||||||
|
@ -250,7 +248,8 @@ def message_edit_query(connection, author, thread_id, post_id):
|
||||||
user = user_resolve(connection, author)
|
user = user_resolve(connection, author)
|
||||||
thread = thread_get(connection, thread_id)
|
thread = thread_get(connection, thread_id)
|
||||||
|
|
||||||
try: message = thread["messages"][post_id]
|
try:
|
||||||
|
message = thread["messages"][post_id]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise BBJParameterError("post_id out of bounds for requested thread")
|
raise BBJParameterError("post_id out of bounds for requested thread")
|
||||||
|
|
||||||
|
@ -418,7 +417,6 @@ def set_admins(connection, users):
|
||||||
not included in `users` will have their privledge
|
not included in `users` will have their privledge
|
||||||
revoked.
|
revoked.
|
||||||
"""
|
"""
|
||||||
connection.execute("UPDATE users SET is_admin = 0")
|
|
||||||
for user in users:
|
for user in users:
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"UPDATE users SET is_admin = 1 WHERE user_name = ?",
|
"UPDATE users SET is_admin = 1 WHERE user_name = ?",
|
||||||
|
|
|
@ -62,7 +62,6 @@ Just like the brackets themselves, backslashes may occur freely within bodies,
|
||||||
they are only removed when they occur before a valid expression.
|
they are only removed when they occur before a valid expression.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from string import punctuation
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
colors = [
|
colors = [
|
||||||
|
@ -74,7 +73,6 @@ markup = [
|
||||||
"bold", "underline", "linequote", "quote", "rainbow"
|
"bold", "underline", "linequote", "quote", "rainbow"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# 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}(.+?)(?<!\\)\*{2}")
|
bold = re.compile(r"(?<!\\)\*{2}(.+?)(?<!\\)\*{2}")
|
||||||
|
@ -133,9 +131,9 @@ def sequential_expressions(string):
|
||||||
but this cannot effectively express an input like
|
but this cannot effectively express an input like
|
||||||
[bold: [red: bolded colors.]], in which case the innermost
|
[bold: [red: bolded colors.]], in which case the innermost
|
||||||
expression will take precedence. For the input:
|
expression will take precedence. For the input:
|
||||||
"[bold: [red: this] is some shit [green: it cant handle]]"
|
"[bold: [red: this] is some shit [green: it can't handle]]"
|
||||||
you get:
|
you get:
|
||||||
[('red', 'this'), ('bold', ' is some shit '), ('green', 'it cant handle')]
|
[('red', 'this'), ('bold', ' is some shit '), ('green', 'it can't handle')]
|
||||||
"""
|
"""
|
||||||
# abandon all hope ye who enter here
|
# abandon all hope ye who enter here
|
||||||
directives = colors + markup
|
directives = colors + markup
|
||||||
|
@ -157,7 +155,7 @@ def sequential_expressions(string):
|
||||||
open_p = False
|
open_p = False
|
||||||
clsd_p = not escaped and nest[-1] != None and char == "]"
|
clsd_p = not escaped and nest[-1] != None and char == "]"
|
||||||
|
|
||||||
# dont splice other directives into linequotes: that is far
|
# don't splice other directives into linequotes: that is far
|
||||||
# too confusing for the client to determine where to put line
|
# too confusing for the client to determine where to put line
|
||||||
# breaks
|
# breaks
|
||||||
if open_p and nest[-1] != "linequote":
|
if open_p and nest[-1] != "linequote":
|
||||||
|
@ -210,7 +208,6 @@ def strip(text):
|
||||||
pass # me the bong im boutta smash tha bish
|
pass # me the bong im boutta smash tha bish
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def entities(text):
|
def entities(text):
|
||||||
"""
|
"""
|
||||||
Returns a tuple where [0] is raw text and [1] is documentation
|
Returns a tuple where [0] is raw text and [1] is documentation
|
||||||
|
@ -220,7 +217,6 @@ def entities(text):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def html(text):
|
def html(text):
|
||||||
"""
|
"""
|
||||||
Returns messages in html format, after being sent through markdown.
|
Returns messages in html format, after being sent through markdown.
|
||||||
|
|
|
@ -43,7 +43,9 @@ def base():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def response(data, usermap={}):
|
def response(data, usermap=None):
|
||||||
|
if usermap is None:
|
||||||
|
usermap = {}
|
||||||
result = base()
|
result = base()
|
||||||
result["data"] = data
|
result["data"] = data
|
||||||
result["usermap"].update(usermap)
|
result["usermap"].update(usermap)
|
||||||
|
|
Loading…
Reference in New Issue