pull/4/head
commit
d23c98e5e7
|
@ -0,0 +1,162 @@
|
|||
Text Entities
|
||||
-------------
|
||||
|
||||
The `entities` attribute is an array of objects that represent blocks
|
||||
of text within a post that have special properties. Clients may safely
|
||||
ignore these things without losing too much meaning, but in a rich
|
||||
application like an Emacs or GUI implementation, they can provide
|
||||
some highlighting and navigation perks. The array object may be
|
||||
empty. If its not, its populated with arrays representing the
|
||||
modifications to be made.
|
||||
|
||||
Objects **always** have a minimum of 3 attributes:
|
||||
```
|
||||
["quote", 5, 7]
|
||||
```
|
||||
object[0] is a string representing the attribute type. They are
|
||||
documented below. The next two items are the indices of the
|
||||
property in the body string. The way clients are to access these
|
||||
indices is beyond the scope of this document; accessing a subsequence
|
||||
varies a lot between programming languages.
|
||||
|
||||
Some objects will provide further arguments beyond those 3.
|
||||
|
||||
|--------------|----------------------------------------------------------|
|
||||
| Name | Description |
|
||||
|--------------|----------------------------------------------------------|
|
||||
| `quote` | This is a string that refers to a previous post number. |
|
||||
| | These are formatted like >>5, which means it is a |
|
||||
| | reference to `post_id` 5. These are not processed in |
|
||||
| | thread OPs. >>0 may be used to refer to the OP. |
|
||||
|--------------|----------------------------------------------------------|
|
||||
| `blockquote` | This is a block of text, denoted by a newline during |
|
||||
| | composure, representing text that is assumed to be |
|
||||
| | a quote of someone else. |
|
||||
|--------------|----------------------------------------------------------|
|
||||
| `color` | This is a block of text, denoted by [[color: body]] |
|
||||
| | during composure. The body may span across newlines. |
|
||||
| | A fourth item is provided in the array: it is one of the |
|
||||
| | following strings representing the color. |
|
||||
| | `red`, `green`, `yellow`, `blue`, `magenta`, or `cyan`. |
|
||||
|--------------|----------------------------------------------------------|
|
||||
| `bold` | Like color, except that no additional attribute is |
|
||||
| | provided. it is denoted as [[bold: body]] during |
|
||||
| | composure. |
|
||||
|--------------|----------------------------------------------------------|
|
||||
|
||||
|
||||
|
||||
Threads & Replies
|
||||
-----------------
|
||||
|
||||
Threads are represented the same when using `thread_index` and
|
||||
`thread_load`, except that the `replies` attribute is only
|
||||
present with `thread_load`. The following attributes are
|
||||
available on the parent object:
|
||||
|
||||
|---------------|------------------------------------------------------|
|
||||
| Name | Description |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `author` | The ID of the author |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `thread_id` | The ID of the thread. |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `title` | The title string of the thread |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `body` | The body of the post's text. |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `entities` | A (possibly empty) array of entity objects for |
|
||||
| | the post `body`. |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `tags` | An array of strings representing tags the |
|
||||
| | author gave to the thread at creation. |
|
||||
| | When empty, it is an array with no elements. |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `replies` | An array containing full reply objects in |
|
||||
| | the order they were posted. Your clients |
|
||||
| | do not need to sort these. |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `reply_count` | An integer representing the number of replies |
|
||||
| | that have been posted in this thread. |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `lastmod` | Unix timestamp of when the thread was last |
|
||||
| | posted in, or a message was edited. |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `edited` | Boolean of whether this post has been edited since |
|
||||
| | it was made. |
|
||||
|---------------|------------------------------------------------------|
|
||||
| `created` | Unix timestamp of when the post was originally made. |
|
||||
|---------------|------------------------------------------------------|
|
||||
|
||||
The following attributes are available on each reply object in `replies`:
|
||||
|
||||
|
||||
|------------|---------------------------------------------------------|
|
||||
| Name | Description |
|
||||
|------------|---------------------------------------------------------|
|
||||
| `post_id` | An integer of the posts ID; unlike thread and user ids, |
|
||||
| | this is not a uuid but instead is incremental, starting |
|
||||
| | from 1 as the first reply and going up by one for each |
|
||||
| | post. |
|
||||
|------------|---------------------------------------------------------|
|
||||
| `author` | Author ID |
|
||||
|------------|---------------------------------------------------------|
|
||||
| `body` | The body the reply's text. |
|
||||
|------------|---------------------------------------------------------|
|
||||
| `entities` | A (possibly empty) array of entity objects for |
|
||||
| | the reply `body`. |
|
||||
|------------|---------------------------------------------------------|
|
||||
| `lastmod` | Unix timestamp of when the post was last edited, or |
|
||||
| | the same as `created` if it never was. |
|
||||
|------------|---------------------------------------------------------|
|
||||
| `edited` | A boolean of whether the post was edited. |
|
||||
|------------|---------------------------------------------------------|
|
||||
| `created` | Unix timestamp of when the reply was originally posted. |
|
||||
|------------|---------------------------------------------------------|
|
||||
|
||||
|
||||
Errors
|
||||
------
|
||||
|
||||
Errors are represented in the `error` field of the response. The error
|
||||
field is always present, but is usually false. If its not false, it is
|
||||
an object with the fields `code` and `description`. `code` is an integer
|
||||
representing the type of failure, and `description` is a string describing
|
||||
the problem. `description` is intended for human consumption; in your client
|
||||
code, use the error codes to handle conditions. The `presentable` column
|
||||
indicates whether the `description` should be shown to users verbatim.
|
||||
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| Code | Presentable | Documentation |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 0 | Never, fix | Malformed json input. `description` is the error |
|
||||
| | your client | string thrown by the server-side json decoder. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 1 | Not a good | Internal server error. Unaltered exception text |
|
||||
| | idea, the | is returned as `description`. This shouldn't |
|
||||
| | exceptions | happen, and if it does, make a bug report. |
|
||||
| | are not | clients should not attempt to intelligently |
|
||||
| | helpful | recover from any errors of this class. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 2 | Nadda. | Unknown `method` was requested. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 3 | Fix. Your. | Missing or malformed parameter values for the |
|
||||
| | Client. | requested `method`. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 4 | Only during | Invalid or unprovided `user`. During |
|
||||
| | registration | registration, this code is returned if the name |
|
||||
| | | is already occupied or contains illegal chars. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 5 | Always | `user` is not registered. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 6 | Always | User `auth_hash` failed or was not provided. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 7 | Always | Requested thread does not exist. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 8 | Always | Requested thread does not allow posts. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 9 | Always | Message edit failed; there is a 24hr limit for |
|
||||
| | | editing posts. |
|
||||
|------|--------------|--------------------------------------------------|
|
||||
| 10 | Always | User action requires `admin` privilege. |
|
||||
|------|--------------|--------------------------------------------------|
|
|
@ -0,0 +1,37 @@
|
|||
(require 'json)
|
||||
|
||||
(defvar bbj:host "localhost")
|
||||
(defvar bbj:port "7066")
|
||||
(defvar bbj:logged-in nil)
|
||||
(defvar bbj:user nil)
|
||||
(defvar bbj:hash nil)
|
||||
|
||||
|
||||
(defun bbj:request (&rest cells)
|
||||
(with-temp-buffer
|
||||
(insert (json-encode cells))
|
||||
(shell-command-on-region
|
||||
(point-min) (point-max)
|
||||
(format "nc %s %s" bbj:host bbj:port)))
|
||||
(with-current-buffer "*Shell Command Output*"
|
||||
(json-read-from-string
|
||||
(buffer-substring-no-properties
|
||||
(point-min) (point-max)))))
|
||||
|
||||
|
||||
(bbj:request '(user . "desvox")
|
||||
'(auth_hash . "nrr")
|
||||
'(method . "check_auth"))
|
||||
|
||||
(defun bbj:login ()
|
||||
(interactive)
|
||||
(setq bbj:user (read-from-minibuffer "(BBJ Username)> "))
|
||||
(if (bbj:request '(method . "is_registered")
|
||||
`(target_user . ,bbj:user))
|
||||
(setq bbj:hash (secure-hash 'sha256 (read-from-minibuffer "(BBJ Password)> ")))
|
||||
(when (y-or-n-p (format "Register for BBJ as %s? " bbj:user))
|
||||
(message
|
||||
(bbj:request (cons 'auth_hash bbj:hash)
|
||||
(cons 'user bbj:user)
|
||||
(cons 'avatar nil)
|
||||
(cons 'bio (read-from-minibuffer "(Enter a short bio about youself!)> ")))))))
|
|
@ -0,0 +1,4 @@
|
|||
from src import schema
|
||||
from src import server
|
||||
|
||||
server.run("localhost", 7066)
|
|
@ -0,0 +1,114 @@
|
|||
from uuid import uuid1
|
||||
from src import schema
|
||||
from time import time
|
||||
from os import path
|
||||
import json
|
||||
|
||||
PATH = "/home/desvox/bbj/"
|
||||
|
||||
if not path.isdir(PATH):
|
||||
path.os.mkdir(PATH, mode=0o744)
|
||||
|
||||
if not path.isdir(path.join(PATH, "threads")):
|
||||
path.os.mkdir(path.join(PATH, "threads"), mode=0o744)
|
||||
|
||||
try:
|
||||
with open(path.join(PATH, "userdb"), "r") as f:
|
||||
USERDB = json.loads(f.read())
|
||||
|
||||
except FileNotFoundError:
|
||||
USERDB = dict(mapname=dict())
|
||||
with open(path.join(PATH, "userdb"), "w") as f:
|
||||
f.write(json.dumps(USERDB))
|
||||
path.os.chmod(path.join(PATH, "userdb"), 0o600)
|
||||
|
||||
### THREAD MANAGEMENT ###
|
||||
|
||||
def thread_index(key="lastmod"):
|
||||
result = list()
|
||||
for ID in path.os.listdir(path.join(PATH, "threads")):
|
||||
thread = thread_load(ID)
|
||||
thread.pop("replies")
|
||||
result.append(thread)
|
||||
return sorted(result, key=lambda i: i[key])
|
||||
|
||||
|
||||
def thread_create(author, body, title, tags):
|
||||
ID = uuid1().hex
|
||||
# make sure None, False, and empty arrays are always repped consistently
|
||||
tags = tags if tags else []
|
||||
scheme = schema.thread(ID, author, body, title, tags)
|
||||
thread_dump(ID, scheme)
|
||||
return scheme
|
||||
|
||||
|
||||
def thread_load(ID):
|
||||
try:
|
||||
with open(path.join(PATH, "threads", ID), "r") as f:
|
||||
return json.loads(f.read())
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
def thread_dump(ID, obj):
|
||||
with open(path.join(PATH, "threads", ID), "w") as f:
|
||||
f.write(json.dumps(obj))
|
||||
|
||||
|
||||
def thread_reply(ID, author, body):
|
||||
thread = thread_load(ID)
|
||||
if not thread:
|
||||
return schema.error(7, "Requested thread does not exist.")
|
||||
|
||||
thread["reply_count"] += 1
|
||||
thread["lastmod"] = time()
|
||||
reply = schema.reply(thread["reply_count"], author, body)
|
||||
thread["replies"].append(reply)
|
||||
thread_dump(ID, thread)
|
||||
return reply
|
||||
|
||||
|
||||
### USER MANAGEMENT ###
|
||||
|
||||
def user_dbdump(dictionary):
|
||||
with open(path.join(PATH, "userdb"), "w") as f:
|
||||
f.write(json.dumps(dictionary))
|
||||
|
||||
|
||||
def user_resolve(name_or_id):
|
||||
check = USERDB.get(name_or_id)
|
||||
try:
|
||||
if check:
|
||||
return name_or_id
|
||||
else:
|
||||
return USERDB["mapname"][name_or_id]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
|
||||
def user_register(auth_hash, name, avatar, bio):
|
||||
if USERDB["mapname"].get(name):
|
||||
return schema.error(4, "Username taken.")
|
||||
|
||||
ID = uuid1().hex
|
||||
scheme = schema.user_internal(ID, auth_hash, name, avatar, bio, False)
|
||||
USERDB.update({ID: scheme})
|
||||
USERDB["mapname"].update({name: ID})
|
||||
user_dbdump(USERDB)
|
||||
return scheme
|
||||
|
||||
|
||||
def user_get(ID):
|
||||
user = USERDB[ID]
|
||||
return schema.user_external(
|
||||
ID, user["name"], user["avatar"],
|
||||
user["bio"], user["admin"])
|
||||
|
||||
|
||||
def user_auth(ID, auth_hash):
|
||||
return auth_hash == USERDB[ID]["auth_hash"]
|
||||
|
||||
|
||||
def user_update(ID, **params):
|
||||
USERDB[ID].update(params)
|
||||
return USERDB[ID]
|
|
@ -0,0 +1,77 @@
|
|||
from src import schema
|
||||
from src import db
|
||||
from json import dumps
|
||||
|
||||
endpoints = {
|
||||
"check_auth": ["user", "auth_hash"],
|
||||
"is_registered": ["target_user"],
|
||||
"thread_load": ["thread_id"],
|
||||
"thread_index": [],
|
||||
"thread_create": ["title", "body", "tags"],
|
||||
"thread_reply": ["thread_id", "body"],
|
||||
"user_register": ["user", "auth_hash", "avatar", "bio"],
|
||||
"user_get": ["target_user"],
|
||||
}
|
||||
|
||||
|
||||
authless = [
|
||||
"is_registered",
|
||||
"user_register"
|
||||
]
|
||||
|
||||
|
||||
def create_usermap(thread, index=False):
|
||||
if index:
|
||||
return {user: db.user_get(user) for user in
|
||||
{i["author"] for i in thread}}
|
||||
|
||||
result = {reply["author"] for reply in thread["replies"]}
|
||||
result.add(thread["author"])
|
||||
return {x: db.user_get(x) for x in result}
|
||||
|
||||
|
||||
def is_registered(json):
|
||||
return dumps(bool(db.USERDB["mapname"].get(json["target_user"])))
|
||||
|
||||
|
||||
def check_auth(json):
|
||||
return dumps(bool(db.user_auth(json["user"], json["auth_hash"])))
|
||||
|
||||
|
||||
def user_register(json):
|
||||
return schema.response(
|
||||
db.user_register(
|
||||
json["auth_hash"],
|
||||
json["user"],
|
||||
json["avatar"],
|
||||
json["bio"]))
|
||||
|
||||
|
||||
def thread_index(json):
|
||||
index = db.thread_index()
|
||||
return schema.response(
|
||||
{"threads": index}, create_usermap(index, True))
|
||||
|
||||
|
||||
def thread_load(json):
|
||||
thread = db.thread_load(json["thread_id"])
|
||||
if not thread:
|
||||
return schema.error(7, "Requested thread does not exist")
|
||||
return schema.response(thread, create_usermap(thread))
|
||||
|
||||
|
||||
def thread_create(json):
|
||||
thread = db.thread_create(
|
||||
json["user"],
|
||||
json["body"],
|
||||
json["title"],
|
||||
json["tags"])
|
||||
return schema.response(thread, create_usermap(thread))
|
||||
|
||||
|
||||
def thread_reply(json):
|
||||
thread = db.thread_reply(
|
||||
json["thread_id"],
|
||||
json["user"],
|
||||
json["body"])
|
||||
return schema.response(thread)
|
|
@ -0,0 +1,77 @@
|
|||
from time import time
|
||||
|
||||
def base():
|
||||
return {
|
||||
"usermap": {},
|
||||
"error": False
|
||||
}
|
||||
|
||||
|
||||
def response(dictionary, usermap=None):
|
||||
result = base()
|
||||
result.update(dictionary)
|
||||
if usermap:
|
||||
result["usermap"] = usermap
|
||||
return result
|
||||
|
||||
|
||||
def error(code, description):
|
||||
result = base()
|
||||
result.update({
|
||||
"error": {
|
||||
"description": description,
|
||||
"code": code
|
||||
}
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def user_internal(ID, auth_hash, name, avatar, bio, admin):
|
||||
return {
|
||||
"user_id": ID,
|
||||
"avatar": avatar,
|
||||
"name": name,
|
||||
"bio": bio,
|
||||
"admin": admin,
|
||||
"auth_hash": auth_hash
|
||||
}
|
||||
|
||||
|
||||
def user_external(ID, name, avatar, bio, admin):
|
||||
return {
|
||||
"user_id": ID,
|
||||
"avatar": avatar,
|
||||
"name": name,
|
||||
"bio": bio,
|
||||
"admin": admin
|
||||
}
|
||||
|
||||
|
||||
def thread(ID, author, body, title, tags):
|
||||
now = time()
|
||||
return {
|
||||
"thread_id": ID,
|
||||
"author": author,
|
||||
"body": body,
|
||||
"entities": list(),
|
||||
"title": title,
|
||||
"tags": tags,
|
||||
"replies": list(),
|
||||
"reply_count": 0,
|
||||
"lastmod": now,
|
||||
"edited": False,
|
||||
"created": now
|
||||
}
|
||||
|
||||
|
||||
def reply(ID, author, body):
|
||||
now = time()
|
||||
return {
|
||||
"post_id": ID,
|
||||
"author": author,
|
||||
"body": body,
|
||||
"entities": list(),
|
||||
"lastmod": now,
|
||||
"edited": False,
|
||||
"created": now
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
from socketserver import StreamRequestHandler, TCPServer
|
||||
from src import endpoints
|
||||
from src import schema
|
||||
from src import db
|
||||
import json
|
||||
|
||||
|
||||
class RequestHandler(StreamRequestHandler):
|
||||
"""
|
||||
Receieves and processes json input; dispatches input to the
|
||||
approproate endpoint, or responds with error objects.
|
||||
"""
|
||||
|
||||
|
||||
def reply(self, dictionary):
|
||||
self.wfile.write(bytes(json.dumps(dictionary), "utf8"))
|
||||
|
||||
|
||||
def handle(self):
|
||||
try:
|
||||
request = json.loads(str(self.rfile.read(), "utf8"))
|
||||
endpoint = request.get("method")
|
||||
|
||||
if endpoint not in endpoints.endpoints:
|
||||
raise IndexError("Invalid endpoint")
|
||||
|
||||
# check to make sure all the arguments for endpoint are provided
|
||||
elif any([key not in request for key in endpoints.endpoints[endpoint]]):
|
||||
raise ValueError("{} requires: {}".format(
|
||||
endpoint, ", ".join(endpoints.endpoints[endpoint])))
|
||||
|
||||
elif endpoint not in endpoints.authless:
|
||||
if not request.get("user"):
|
||||
raise ConnectionError("No username provided.")
|
||||
|
||||
user = db.user_resolve(request["user"])
|
||||
request["user"] = user
|
||||
|
||||
if not user:
|
||||
raise ConnectionAbortedError("User not registered")
|
||||
|
||||
elif endpoint != "check_auth" and not db.user_auth(user, request.get("auth_hash")):
|
||||
raise ConnectionRefusedError("Authorization failed.")
|
||||
|
||||
except json.decoder.JSONDecodeError as E:
|
||||
return self.reply(schema.error(0, str(E)))
|
||||
|
||||
except IndexError as E:
|
||||
return self.reply(schema.error(2, str(E)))
|
||||
|
||||
except ValueError as E:
|
||||
return self.reply(schema.error(3, str(E)))
|
||||
|
||||
except ConnectionError as E:
|
||||
return self.reply(schema.error(4, str(E)))
|
||||
|
||||
except ConnectionAbortedError as E:
|
||||
return self.reply(schema.error(5, str(E)))
|
||||
|
||||
except ConnectionRefusedError as E:
|
||||
return self.reply(schema.error(6, str(E)))
|
||||
|
||||
except Exception as E:
|
||||
return self.reply(schema.error(1, str(E)))
|
||||
|
||||
try:
|
||||
self.reply(eval("endpoints." + endpoint)(request))
|
||||
except Exception as E:
|
||||
self.reply(schema.error(1, str(E)))
|
||||
|
||||
|
||||
def run(host, port):
|
||||
server = TCPServer((host, port), RequestHandler)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("bye")
|
||||
server.server_close()
|
Loading…
Reference in New Issue