Compare commits

..

5 Commits

Author SHA1 Message Date
Ben Harris 1538aad56e
fix some typos and warnings 2022-01-15 22:54:16 -05:00
khuxkm 9488ade7fb Fix config.json.example 2019-10-08 14:31:59 +00:00
Ben Harris ee85dfe5ab fix up if's in setup.sh 2019-03-03 17:23:55 -05:00
Ben Harris 5c708b436b tildeverse 2019-02-26 10:51:20 -05:00
Ben Harris 68253447b7 Set theme jekyll-theme-midnight 2018-11-09 12:17:09 -05:00
22 changed files with 499 additions and 576 deletions

View File

@ -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 stand alone 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
```

View File

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

View File

@ -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
@ -335,15 +328,15 @@ class BBJ(object):
is_okay = bbj.validate_credentials("desvox", hashed_password, exception=False) is_okay = bbj.validate_credentials("desvox", hashed_password, exception=False)
""" """
self.validate_all([ self.validate_all([
("user_name", user_name), ("user_name", user_name),
("auth_hash", user_auth) ("auth_hash", user_auth)
], ValueError) ], ValueError)
try: try:
response = self("check_auth", response = self("check_auth",
no_auth=True, no_auth=True,
target_user=user_name, target_user=user_name,
target_hash=user_auth target_hash=user_auth
) )
return response["data"] return response["data"]
except ConnectionRefusedError as e: except ConnectionRefusedError as e:
@ -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
@ -391,10 +382,10 @@ class BBJ(object):
user_auth = sha256(bytes(user_auth, "utf8")).hexdigest() user_auth = sha256(bytes(user_auth, "utf8")).hexdigest()
response = self("user_register", response = self("user_register",
no_auth=True, no_auth=True,
user_name=user_name, user_name=user_name,
auth_hash=user_auth auth_hash=user_auth
)["data"] )["data"]
assert all([ assert all([
user_auth == response["auth_hash"], user_auth == response["auth_hash"],
@ -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.
@ -466,10 +453,9 @@ class BBJ(object):
print(message["body"]) print(message["body"])
""" """
response = self("thread_load", response = self("thread_load",
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.

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,6 @@
"port": 7099, "port": 7099,
"host": "127.0.0.1", "host": "127.0.0.1",
"instance_name": "BBJ", "instance_name": "BBJ",
"allow_anon": True, "allow_anon": true,
"debug": False "debug": false
} }

View File

@ -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

2
contrib/bbj.sh 100644
View File

@ -0,0 +1,2 @@
#!/bin/sh
exec /srv/bbj/venv/bin/python3 /srv/bbj/clients/urwid/main.py

1
docs/_config.yml 100644
View File

@ -0,0 +1 @@
theme: jekyll-theme-midnight

View File

@ -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)

View File

@ -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.

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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 ..

View File

@ -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):

View File

@ -1,26 +1,25 @@
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"],
"is_registered": ["target_user"], "is_registered": ["target_user"],
"is_admin": ["target_user"], "is_admin": ["target_user"],
"thread_index": [], "thread_index": [],
"thread_load": ["thread_id"], "thread_load": ["thread_id"],
"thread_create": ["title", "body", "tags"], "thread_create": ["title", "body", "tags"],
"thread_reply": ["thread_id", "body"], "thread_reply": ["thread_id", "body"],
"edit_post": ["thread_id", "post_id", "body"], "edit_post": ["thread_id", "post_id", "body"],
"edit_query": ["thread_id", "post_id"], "edit_query": ["thread_id", "post_id"],
"can_edit": ["thread_id", "post_id"], "can_edit": ["thread_id", "post_id"],
"user_register": ["user", "auth_hash", "quip", "bio"], "user_register": ["user", "auth_hash", "quip", "bio"],
"user_get": ["target_user"], "user_get": ["target_user"],
"user_name_to_id": ["target_user"] "user_name_to_id": ["target_user"]
} }
authless = [ authless = [
"is_registered", "is_registered",
"user_register" "user_register"
@ -32,7 +31,7 @@ authless = [
def create_usermap(thread, index=False): def create_usermap(thread, index=False):
if index: if index:
return {user: db.user_get(user) for user in return {user: db.user_get(user) for user in
{i["author"] for i in thread}} {i["author"] for i in thread}}
result = {reply["author"] for reply in thread["replies"]} result = {reply["author"] for reply in thread["replies"]}
result.add(thread["author"]) result.add(thread["author"])
@ -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"]))

3
requirements.txt 100644
View File

@ -0,0 +1,3 @@
CherryPy~=18.6.1
Markdown~=3.3.6
urwid~=2.1.2

View File

@ -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:
@ -171,7 +173,7 @@ def validate(json, args):
raise BBJParameterError( raise BBJParameterError(
"Required parameter {} is absent from the request. " "Required parameter {} is absent from the request. "
"This method requires the following arguments: {}" "This method requires the following arguments: {}"
.format(arg, ", ".join(args))) .format(arg, ", ".join(args)))
def no_anon_hook(user, message=None, user_error=True): def no_anon_hook(user, message=None, user_error=True):
@ -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,12 +677,13 @@ 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."),
("value", "VARIES: the object for which `key` will check for."), ("value", "VARIES: the object for which `key` will check for."),
("OPTIONAL: error", "boolean: when `true`, will return an API error " ("OPTIONAL: error", "boolean: when `true`, will return an API error "
"response instead of a special object.") "response instead of a special object.")
) )

View File

@ -1,13 +1,13 @@
#!/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)
cat <<EOF cat <<EOF
This script initializes the deps and files for bbj and also sets up its database. This script initializes the deps and files for bbj and also sets up its database.
It takes the following flags: It takes the following flags:
--help to print this --help to print this
@ -15,20 +15,25 @@ It takes the following flags:
You can optionally pass a different python interpreter to use (such as You can optionally pass a different python interpreter to use (such as
a virtual environment), with no arguments this will use the system python3 a virtual environment), with no arguments this will use the system python3
EOF EOF
exit;; exit;;
--dbset )
sqlite3 data.sqlite < schema.sql --dbset)
echo cleared create_db
chmod 600 data.sqlite exit;;
exit;;
esac esac
PYTHON=`which python3` [ -e logs ] || mkdir -p logs/exceptions
[[ -e logs ]] || mkdir logs; mkdir logs/exceptions
[[ -z $1 ]] || PYTHON=$1 PYTHON=$(which python3)
echo Using $PYTHON... [ -z "$1" ] || PYTHON="$1"
$PYTHON -m pip install ${DEPS[*]} printf "Using %s...\n" "$PYTHON"
echo "Enter [i] to initialize a new database" $PYTHON -m pip install -r requirements.txt
read CLEAR
[[ $CLEAR == "i" ]] && sqlite3 data.sqlite < schema.sql; chmod 600 data.sqlite printf "Enter [i] to initialize a new database\n"
read -r CLEAR
if [ "$CLEAR" = "i" ]; then
create_db
fi

View File

@ -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
@ -58,15 +57,15 @@ def message_feed(connection, time):
""" """
threads = { threads = {
obj[0]: schema.thread(*obj) for obj in obj[0]: schema.thread(*obj) for obj in
connection.execute( connection.execute(
"SELECT * FROM threads WHERE last_mod > ?", (time,)) "SELECT * FROM threads WHERE last_mod > ?", (time,))
} }
messages = list() messages = list()
for thread in threads.values(): for thread in threads.values():
messages += [ messages += [
schema.message(*obj) for obj in schema.message(*obj) for obj in
connection.execute(""" connection.execute("""
SELECT * FROM messages WHERE thread_id = ? SELECT * FROM messages WHERE thread_id = ?
AND created > ? """, (thread["thread_id"], time)) AND created > ? """, (thread["thread_id"], time))
] ]
@ -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()]
@ -121,7 +119,7 @@ def thread_index(connection, include_op=False):
threads = [ threads = [
thread_get(connection, obj[0], False, include_op) thread_get(connection, obj[0], False, include_op)
for obj in c.fetchall() for obj in c.fetchall()
] ]
return threads return threads
@ -146,7 +144,7 @@ def thread_create(connection, author_id, body, title, send_raw=False):
Create a new thread and return it. Create a new thread and return it.
""" """
validate([ validate([
("body", body), ("body", body),
("title", title) ("title", title)
]) ])
@ -154,7 +152,7 @@ def thread_create(connection, author_id, body, title, send_raw=False):
thread_id = uuid1().hex thread_id = uuid1().hex
scheme = schema.thread( scheme = schema.thread(
thread_id, author_id, title, thread_id, author_id, title,
now, now, -1, # see below for why i set -1 instead of 0 now, now, -1, # see below for why i set -1 instead of 0
False, author_id) False, author_id)
connection.execute(""" connection.execute("""
@ -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")
@ -364,7 +363,7 @@ def user_resolve(connection, name_or_id, externalize=False, return_false=True):
SELECT * FROM users SELECT * FROM users
WHERE user_name = ? WHERE user_name = ?
OR user_id = ? """, OR user_id = ? """,
(name_or_id, name_or_id)).fetchone() (name_or_id, name_or_id)).fetchone()
if user: if user:
user = schema.user_internal(*user) user = schema.user_internal(*user)
@ -397,8 +396,8 @@ def user_update(connection, user_object, parameters):
user_object[key] = value user_object[key] = value
values = ordered_keys(user_object, values = ordered_keys(user_object,
"user_name", "quip", "auth_hash", "user_name", "quip", "auth_hash",
"bio", "color", "user_id") "bio", "color", "user_id")
connection.execute(""" connection.execute("""
UPDATE users SET UPDATE users SET
@ -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 = ?",

View File

@ -62,11 +62,10 @@ 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 = [
#0, 1 2 3 4 5 6 dim is not used in color api # 0, 1 2 3 4 5 6 dim is not used in color api
"red", "yellow", "green", "blue", "cyan", "magenta", "dim" "red", "yellow", "green", "blue", "cyan", "magenta", "dim"
] ]
@ -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
@ -151,13 +149,13 @@ def sequential_expressions(string):
continue continue
if not escaped and char == "[": if not escaped and char == "[":
directive = paragraph[index+1:paragraph.find(": ", index+1)] directive = paragraph[index + 1:paragraph.find(": ", index + 1)]
open_p = directive in directives open_p = directive in directives
else: else:
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":
@ -207,8 +205,7 @@ def strip(text):
Returns the text with all formatting directives removed. Returns the text with all formatting directives removed.
Not to be confused with `raw`. Not to be confused with `raw`.
""" """
pass # me the bong im boutta smash tha bish pass # me the bong im boutta smash tha bish
def entities(text): def entities(text):
@ -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.

View File

@ -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)