diff --git a/document_endpoints.py b/document_endpoints.py new file mode 100644 index 0000000..71d2e31 --- /dev/null +++ b/document_endpoints.py @@ -0,0 +1,125 @@ +""" +This is a small script that creates the endpoint doc page. It should be +evoked from the command line each time changes are made. It writes +to ./documentation/docs/api_overview.md + +The code used in this script is the absolute minimum required to +get the job done; it can be considered a crude hack at best. I am +more interested in writing good documentation than making sure that +the script that shits it out is politcally correct ;) +""" + +from server import API +import pydoc + +body = """ +# How to BBJ? + +## Input + +BBJ is interacted with entirely through POST requests, whose bodies are +json objects. + +The endpoints, all listed below, can be contacted at the path /api/ relative +to the root of where BBJ is hosted. If bbj is hosted on a server on port 80 +at the root: + +`http://server.com/api/endpoint_here` + +The body of your request contains all of it's argument fields, instead of +using URL parameters. As a demonstration, to call `thread_create`, +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 +passed into the API for that spot. Your input will look like this: + +```json +{ + "title": "Hello world!!", + "body": "Hi! I am exploring this cool board thing!!" +} +``` + +And you will POST this body to `http://server.com:PORT/api/thread_create`. + +A few endpoints do not require any arguments. These can still be POSTed to, +but the body may be completely empty or an empty json object. You can even +GET these if you so choose. + +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 +not clog up any pipes :) In the same vein, endpoints who dont take arguments +don't care if you supply them anyway. + +## Output + +BBJ returns data in a consistently formatted json object. The base object +has three keys: `data`, `usermap`, and `error`. Visualizied: + +```javascript +{ + "error": false, // boolean false or error object + "data": null, // null or the requested data from endpoint. + "usermap": {} // potentially empty object, maps user_ids to user objects +} + +// If "error" is true, it looks like this: + +{ + "error": { + "code": // an integer from 0 to 5, + "description": // a string describing the error in detail. + } + "data": null // ALWAYS null if error is not false + "usermap": {} // ALWAYS empty if error is not false +} +``` + +### data + +`data` is what the endpoint actually returns. The type of contents vary +by endpoint and are documented below. If an endpoint says it returns a +boolean, it will look like `"data": True`. If it says it returns an array, +it will look like `"data": ["stuff", "goes", "here"]` + +### usermap + +The usermap is a json object mapping user_ids within `data` to full user +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 +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 +are user objects. It should be noted that the anonymous user has it's own +ID and profile object as well. + +### error + +`error` is typically `false`. If it is __not__ false, then the request failed +and the json object that `error` contains should be inspected. (see the above +visualation) Errors follow a strict code system, making it easy for your client +to map these responses to native exception types or signals in your language of +choice. See [the full error page](errors.md) for details. + + +""" + +endpoints = [ + ref for name, ref in API.__dict__.items() + if hasattr(ref, "exposed") +] + +types = { + function.doctype: list() for function in endpoints +} + +for function in endpoints: + types[function.doctype].append(function) + +for doctype in sorted(types.keys()): + body += "# %s\n\n" % doctype + funcs = sorted(types[doctype], key=lambda _: _.__name__) + for f in funcs: + body += "## %s\n\n%s\n\n" % (f.__name__, pydoc.getdoc(f)) + body += "\n\n--------\n\n" + +with open("documentation/docs/api_overview.md", "w") as output: + output.write(body) diff --git a/documentation/docs/api_overview.md b/documentation/docs/api_overview.md new file mode 100644 index 0000000..3f31e9d --- /dev/null +++ b/documentation/docs/api_overview.md @@ -0,0 +1,315 @@ + +# How to BBJ? + +## Input + +BBJ is interacted with entirely through POST requests, whose bodies are +json objects. + +The endpoints, all listed below, can be contacted at the path /api/ relative +to the root of where BBJ is hosted. If bbj is hosted on a server on port 80 +at the root: + +`http://server.com/api/endpoint_here` + +The body of your request contains all of it's argument fields, instead of +using URL parameters. As a demonstration, lets call `thread_create`. +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 +passed into the API for that spot. Your input will look like this: + +```json +{ + "title": "Hello world!!", + "body": "Hi! I am exploring this cool board thing!!" +} +``` + +And you will POST this body to `http://server.com:PORT/api/thread_create`. + +A few endpoints do not require any arguments. These can still be POSTed to, +but the body may be completely empty or an empty json object. You can even +GET these if you so choose. + +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 +not clog up any pipes :) In the same vein, endpoints who dont take arguments +don't care if you supply them anyway. + +## Output + +BBJ returns data in a consistently formatted json object. The base object +has three keys: `data`, `usermap`, and `error`. Visualizied: + +```javascript +{ + "error": false, // boolean false or error object + "data": null, // null or the requested data from endpoint. + "usermap": {} // a potentially empty object mapping user_ids to their objects +} + +// If "error" is true, it looks like this: + +{ + "error": { + "code": // an integer from 0 to 5, + "description": // a string describing the error in detail. + } + "data": null // ALWAYS null if error is not false + "usermap": {} // ALWAYS empty if error is not false +} +``` + +### data + +`data` is what the endpoint actually returns. The type of contents vary +by endpoint and are documented below. If an endpoint says it returns a +boolean, it will look like `"data": True`. If it says it returns an array, +it will look like `"data": ["stuff", "goes", "here"]` + +### usermap + +The usermap is a json object mapping user_ids within `data` to full user +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 +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 +are user objects. It should be noted that the anonymous user has it's own +ID and profile object as well. + +### error + +`error` is typically `null`. If it is __not__ null, then the request failed +and the json object that `error` contains should be inspected. (see the above +visualation) Errors follow a strict code system, making it easy for your client +to map these responses to native exception types or signals in your language of +choice. See [the full error page](errors.md) for details. + + +# Authorization + +## check_auth + +Takes the arguments `target_user` and `target_hash`, and +returns boolean true or false whether the hash is valid. + + + +-------- + +# Threads & Messages + +## delete_post + +Requires the arguments `thread_id` and `post_id`. + +Delete a message from a thread. The same rules apply +here as `edit_post` and `edit_query`: the logged in user +must either be the one who posted the message within 24hrs, +or have admin rights. The same error descriptions and code +are returned on falilure. Boolean true is returned on +success. + +If the post_id is 0, the whole thread is deleted. + +## edit_post + +Replace a post with a new body. Requires the arguments +`thread_id`, `post_id`, and `body`. This method verifies +that the user can edit a post before commiting the change, +otherwise an error object is returned whose description +should be shown to the user. + +To perform sanity checks and retrieve the unformatted body +of a post without actually attempting to replace it, use +`edit_query` first. + +Optionally you may also include the argument `send_raw` to +set the message's formatting flag. However, if this is the +only change you would like to make, you should use the +endpoint `set_post_raw` instead. + +Returns the new message object. + +## edit_query + +Queries the database to ensure the user can edit a given +message. Requires the arguments `thread_id` and `post_id` +(does not require a new body) + +Returns the original message object without any formatting +on success. Returns a descriptive code 4 otherwise. + +## message_feed + +Returns a special object representing all activity on the board since +the argument `time`, a unix/epoch timestamp. + +{ + "threads": { + "thread_id": { + ...thread object + }, + ...more thread_id/object pairs + }, + "messages": [...standard message object array sorted by date] +} + +The message objects in "messages" are the same objects returned +in threads normally. They each have a thread_id parameter, and +you can access metadata for these threads by the "threads" object +which is also provided. + +The "messages" array is already sorted by submission time, newest +first. The order in the threads object is undefined and you should +instead use their `last_mod` attribute if you intend to list them +out visually. + +You may optionally provide a `format` argument: this is treated +the same way as the `thread_load` endpoint and you should refer +to its documentation for more info. + +## set_post_raw + +Requires the boolean argument of `value`, string argument +`thread_id`, and integer argument `post_id`. `value`, when false, +means that the message will be passed through message formatters +before being sent to clients. When `value` is true, this means +it will never go through formatters, all of its whitespace is +sent to clients verbatim and expressions are not processed. + +The same rules for editing messages (see `edit_query`) apply here +and the same error objects are returned for violations. + +You may optionally set this value as well when using `edit_post`, +but if this is the only change you want to make to the message, +using this endpoint instead is preferable. + +## set_thread_pin + +Requires the arguments `thread_id` and `value`. `value` +must be a boolean of what the pinned status should be. +This method requires that the caller is logged in and +has admin status on their account. + +Returns the same boolean you supply as `value` + +## thread_create + +Creates a new thread and returns it. Requires the non-empty +string arguments `body` and `title`. + +If the argument `send_raw` is specified and has a non-nil +value, the OP message will never recieve special formatting. + +## thread_index + +Return an array with all the threads, ordered by most recent activity. +Requires no arguments. + +Optionally, you may supply the argument `include_op`, which, when +non-nil, will include a "messages" key with the object, whose sole +content is the original message (post_id 0). + +## thread_load + +Returns the thread object with all of its messages loaded. +Requires the argument `thread_id`. `format` may also be +specified as a formatter to run the messages through. +Currently only "sequential" is supported. + +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) + +## thread_reply + +Creates a new reply for the given thread and returns it. +Requires the string arguments `thread_id` and `body` + +If the argument `send_raw` is specified and has a non-nil +value, the message will never recieve special formatting. + + + +-------- + +# Tools + +## db_validate + +Requires the arguments `key` and `value`. Returns an object +with information about the database sanity criteria for +key. This can be used to validate user input in the client +before trying to send it to the server. + +If the argument `error` is supplied with a non-nil value, +the server will return a standard error object on failure +instead of the special object described below. + +The returned object has two keys: + +{ + "bool": true/false, + "description": null/"why this value is bad" +} + +If bool == false, description is a string describing the +problem. If bool == true, description is null and the +provided value is safe to use. + +## format_message + +Requires the arguments `body` and `format`. Applies +`format` to `body` and returns the new object. See +`thread_load` for supported specifications for `format`. + +## user_map + +Returns an array with all registered user_ids, with the usermap +object populated by their full objects. + + + +-------- + +# Users + +## get_me + +Requires no arguments. Returns your internal user object, +including your authorization hash. + +## is_admin + +Requires the argument `target_user`. Returns a boolean +of whether that user is an admin. + +## user_get + +Retreive an external user object for the given `user`. +Can be a user_id or user_name. + +## user_is_registered + +Takes the argument `target_user` and returns true or false +whether they are in the system or not. + +## user_register + +Register a new user into the system and return the new object. +Requires the string arguments `user_name` and `auth_hash`. +Do not send User/Auth headers with this method. + +## user_update + +Receives new parameters and assigns them to the user_object +in the database. The following new parameters can be supplied: +`user_name`, `auth_hash`, `quip`, `bio`, and `color`. Any number +of them may be supplied. + +The newly updated user object is returned on success. + + + +-------- + diff --git a/documentation/docs/index.md b/documentation/docs/index.md index da37213..4776965 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -1,17 +1,19 @@ -# Welcome to MkDocs +# Bulletin Butter & Jelly +## A simple community textboard +### BBJ is trivial collection of python scripts and database queries that miraculously shit out a fully functional client-server textboard. -For full documentation visit [mkdocs.org](http://mkdocs.org). +See also: the [GitHub repository](https://github.com/desvox/bbj). -## Commands +BBJ is heavily inspired by image boards like 4chan, but it offers a simple +account system to allow users to identify themselves and set profile +attributes like a more traditional forum. Registration is optional and there +are only minimal restrictions on anonymous participation. -* `mkdocs new [dir-name]` - Create a new project. -* `mkdocs serve` - Start the live-reloading docs server. -* `mkdocs build` - Build the documentation site. -* `mkdocs help` - Print this help message. +![screenshot](https://tilde.town/~desvox/bbj/screenshot.png) -## Project layout +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 +content like imageboards. It has very few dependancies and is easy to set up. - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - ... # Other markdown pages, images and other files. +The API is simple and doesn't use require complex authorization schemes or session management. +It is fully documented on this site. diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index c97182f..2feacb8 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -1 +1 @@ -site_name: My Docs +site_name: BBJ API Documentation diff --git a/server.py b/server.py index 45a07cc..232e842 100644 --- a/server.py +++ b/server.py @@ -20,6 +20,7 @@ app_config = { "debug": False } + try: with open("config.json") as _conf: app_config.update(json.load(_conf)) @@ -191,6 +192,11 @@ class API(object): validate(args, ["user_name", "auth_hash"]) return db.user_register( database, args["user_name"], args["auth_hash"]) + user_register.doctype = "Users" + user_register.args = [ + ("user_name", "string: the desired display name"), + ("auth_hash", "string: a sha256 hash of a password") + ] @api_method def user_update(self, args, database, user, **kwargs): @@ -205,6 +211,15 @@ class API(object): no_anon_hook(user, "Anons cannot modify their account.") validate(args, []) # just make sure its not empty return db.user_update(database, user, args) + user_update.doctype = "Users" + user_update.args = [ + ("Any of the following may be submitted:", ""), + ("user_name", "string: a desired display name"), + ("auth_hash", "string: sha256 hash for a new password"), + ("quip", "string: a short string that can be used as a signature"), + ("bio", "string: a user biography for their profile"), + ("color", "integer: 0-6, a display color for the user") + ] @api_method def get_me(self, args, database, user, **kwargs): @@ -213,6 +228,8 @@ class API(object): including your authorization hash. """ return user + get_me.doctype = "Users" + get_me.args = [("", "")] @api_method def user_map(self, args, database, user, **kwargs): @@ -232,16 +249,22 @@ class API(object): for user in users } return list(users) + user_map.doctype = "Tools" + user_map.args = [("", "")] @api_method def user_get(self, args, database, user, **kwargs): """ - Retreive an external user object for the given `user`. + Retreive an external user object for the given `target_user`. Can be a user_id or user_name. """ - validate(args, ["user"]) + validate(args, ["target_user"]) return db.user_resolve( - database, args["user"], return_false=False, externalize=True) + database, args["target_user"], return_false=False, externalize=True) + user_get.doctype = "Users" + user_get.args = [ + ("user", "string: either a user_name or a user_id") + ] @api_method def user_is_registered(self, args, database, user, **kwargs): @@ -251,6 +274,8 @@ class API(object): """ validate(args, ["target_user"]) return bool(db.user_resolve(database, args["target_user"])) + user_is_registered.doctype = "Users" + # user_is_registered.args = @api_method def check_auth(self, args, database, user, **kwargs): @@ -262,6 +287,7 @@ class API(object): user = db.user_resolve( database, args["target_user"], return_false=False) return args["target_hash"].lower() == user["auth_hash"].lower() + check_auth.doctype = "Authorization" @api_method def thread_index(self, args, database, user, **kwargs): @@ -277,6 +303,7 @@ class API(object): threads = db.thread_index(database, include_op=op) cherrypy.thread_data.usermap = create_usermap(database, threads, True) return threads + thread_index.doctype = "Threads & Messages" @api_method def message_feed(self, args, database, user, **kwargs): @@ -317,6 +344,7 @@ class API(object): do_formatting(args.get("format"), feed["messages"]) return feed + message_feed.doctype = "Threads & Messages" @api_method def thread_create(self, args, database, user, **kwargs): @@ -335,6 +363,7 @@ class API(object): cherrypy.thread_data.usermap = \ create_usermap(database, thread["messages"]) return thread + thread_create.doctype = "Threads & Messages" @api_method def thread_reply(self, args, database, user, **kwargs): @@ -350,6 +379,7 @@ class API(object): return db.thread_reply( database, user["user_id"], args["thread_id"], args["body"], args.get("send_raw")) + thread_reply.doctype = "Threads & Messages" @api_method def thread_load(self, args, database, user, **kwargs): @@ -369,6 +399,7 @@ class API(object): create_usermap(database, thread["messages"]) do_formatting(args.get("format"), thread["messages"]) return thread + thread_load.doctype = "Threads & Messages" @api_method def edit_post(self, args, database, user, **kwargs): @@ -395,6 +426,7 @@ class API(object): return db.message_edit_commit( database, user["user_id"], args["thread_id"], args["post_id"], args["body"], args.get("send_raw")) + edit_post.doctype = "Threads & Messages" @api_method def delete_post(self, args, database, user, **kwargs): @@ -414,6 +446,7 @@ class API(object): validate(args, ["thread_id", "post_id"]) return db.message_delete( database, user["user_id"], args["thread_id"], args["post_id"]) + delete_post.doctype = "Threads & Messages" @api_method def set_post_raw(self, args, database, user, **kwargs): @@ -438,6 +471,7 @@ class API(object): database, user["user_id"], args["thread_id"], args["post_id"], None, args["value"], None) + set_post_raw.doctype = "Threads & Messages" @api_method def is_admin(self, args, database, user, **kwargs): @@ -449,6 +483,7 @@ class API(object): user = db.user_resolve( database, args["target_user"], return_false=False) return user["is_admin"] + is_admin.doctype = "Users" @api_method def edit_query(self, args, database, user, **kwargs): @@ -464,6 +499,7 @@ class API(object): validate(args, ["thread_id", "post_id"]) return db.message_edit_query( database, user["user_id"], args["thread_id"], args["post_id"]) + edit_query.doctype = "Threads & Messages" @api_method def format_message(self, args, database, user, **kwargs): @@ -476,6 +512,7 @@ class API(object): message = [{"body": args["body"]}] do_formatting(args["format"], message) return message[0]["body"] + format_message.doctype = "Tools" @api_method def set_thread_pin(self, args, database, user, **kwargs): @@ -491,6 +528,7 @@ class API(object): if not user["is_admin"]: raise BBJUserError("Only admins can set thread pins") return db.set_thread_pin(database, args["thread_id"], args["value"]) + set_thread_pin.doctype = "Threads & Messages" @api_method def db_validate(self, args, database, user, **kwargs): @@ -527,6 +565,7 @@ class API(object): response["bool"] = False response["description"] = e.description return response + db_validate.doctype = "Tools" def api_http_error(status, message, traceback, version):