{{title}}
3 |{{summary}}
4 |├── .gitignore ├── LICENSE ├── Makejail ├── README.md ├── appjail-files ├── config.json └── pkg.latest.conf ├── clients ├── emacs │ └── bbj.el ├── network_client.py └── urwid │ ├── main.py │ └── network.py ├── config.json.example ├── css ├── 9x1.css ├── base.css └── themeassets │ ├── ms_sans_serif.woff │ ├── ms_sans_serif.woff2 │ ├── ms_sans_serif_bold.woff │ ├── ms_sans_serif_bold.woff2 │ ├── redblocks.bmp │ └── waves.bmp ├── dbupdate.py ├── docs ├── _config.yml ├── docs │ ├── api_overview.md │ ├── errors.md │ ├── img │ │ └── screenshot.png │ ├── index.md │ └── validation.md ├── mkdocs.yml └── site │ ├── 404.html │ ├── api_overview │ └── index.html │ ├── css │ ├── base.css │ ├── bootstrap-custom.min.css │ ├── font-awesome-4.5.0.css │ └── highlight.css │ ├── errors │ └── index.html │ ├── fonts │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 │ ├── img │ ├── favicon.ico │ ├── grid.png │ └── screenshot.png │ ├── index.html │ ├── js │ ├── base.js │ ├── bootstrap-3.0.3.min.js │ ├── highlight.pack.js │ └── jquery-1.10.2.min.js │ ├── mkdocs │ ├── js │ │ ├── lunr.min.js │ │ ├── mustache.min.js │ │ ├── require.js │ │ ├── search-results-template.mustache │ │ ├── search.js │ │ └── text.js │ └── search_index.json │ ├── sitemap.xml │ └── validation │ └── index.html ├── gendocs.sh ├── js ├── datetime.js └── postboxes.js ├── mkendpoints.py ├── prototype ├── clients │ ├── elisp │ │ └── bbj.el │ ├── network_client.py │ └── urwid │ │ ├── main.py │ │ └── src │ │ ├── network.py │ │ └── widgets.py ├── docs │ └── protocol.org ├── main.py └── src │ ├── db.py │ ├── endpoints.py │ ├── formatting.py │ ├── schema.py │ └── server.py ├── readme.png ├── schema.sql ├── server.py ├── setup.sh ├── src ├── db.py ├── exceptions.py ├── formatting.py ├── schema.py └── utils.py ├── templates ├── account.html ├── threadIndex.html └── threadLoad.html └── todo.org /.gitignore: -------------------------------------------------------------------------------- 1 | /*.db 2 | /config.json 3 | *.sqlite 4 | *__pycache__* 5 | /logs/ 6 | /bin/ 7 | /lib/ 8 | /lib64 9 | /pyvenv.cfg 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Blake DeMarcy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makejail: -------------------------------------------------------------------------------- 1 | ARG bbj_config=appjail-files/config.json 2 | 3 | OPTION overwrite=force 4 | OPTION start 5 | OPTION volume=bbj-data mountpoint:/bbj/data owner:1001 group:1001 6 | 7 | INCLUDE gh+DtxdF/efficient-makejail 8 | 9 | CMD mkdir -p /usr/local/etc/pkg/repos 10 | COPY appjail-files/pkg.latest.conf /usr/local/etc/pkg/repos/Latest.conf 11 | 12 | PKG python py311-pip py311-sqlite3 git-tiny 13 | 14 | CMD pw useradd -n bbj -d /bbj -c "bulletin board server for small communities" 15 | CMD mkdir -p /bbj /bbj/data 16 | CMD chown bbj:bbj /bbj /bbj/data 17 | 18 | USER bbj 19 | 20 | WORKDIR /bbj 21 | 22 | RUN git clone --depth 1 https://github.com/DtxdF/bbj.git src 23 | 24 | COPY ${bbj_config} src/config.json 25 | CMD chown bbj:bbj /bbj/src/config.json 26 | 27 | RUN pip install cherrypy 28 | RUN pip install urwid 29 | RUN pip install jinja2 30 | 31 | CMD if [ ! -f /bbj/data/bbj.db ]; then sqlite3 /bbj/data/bbj.db < /bbj/src/schema.sql; fi 32 | CMD chown bbj:bbj /bbj/data/bbj.db 33 | CMD chmod 600 /bbj/data/bbj.db 34 | 35 | STOP 36 | 37 | STAGE start 38 | 39 | USER bbj 40 | WORKDIR /bbj/src 41 | 42 | RUN daemon \ 43 | -t "bulletin board server for small communities" \ 44 | -p /bbj/data/pid \ 45 | -o /bbj/data/log \ 46 | python server.py 47 | 48 | STAGE custom:bbj_status 49 | 50 | CMD if [ -f "/bbj/data/pid" ]; then \ 51 | top -ap `head -1 /bbj/data/pid`; \ 52 | fi 53 | 54 | STAGE custom:bbj_log 55 | 56 | CMD if [ -f "/bbj/data/log" ]; then \ 57 | less -R /bbj/data/log; \ 58 | fi 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bulletin Butter & Jelly 2 | 3 | BBJ is a trivial collection of python scripts and database queries that 4 | create a fully functional, text-driven community bulletin board. 5 | Requires Python 3.4 and up for the server and the official TUI client (clients/urwid/). 6 | 7 |  8 |
Page not found
87 |Errors in BBJ are separated into 6 different codes, to allow easy mapping to 101 | native exception and signaling systems available in the client's programming 102 | language. Errors are all or nothing, there are no "warnings". If a response has 103 | a non-false error field, then data will always be null. An error response from 104 | the api looks like this...
105 |{
106 | "error": {
107 | "code": // an integer from 0 to 5,
108 | "description": // a string describing the error in detail.
109 | }
110 | "data": null // ALWAYS null if error is not false
111 | "usermap": {} // ALWAYS empty if error is not false
112 | }
113 |
114 |
115 | The codes split errors into categories. Some are oriented 116 | to client developers while others should be shown directly to 117 | users.
118 |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.
121 |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.
124 |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.
127 |Code 3: Parameter error: client sent erroneous input for its method. This could mean missing arguments, type errors, etc. It generalizes errors that should be fixed by the client developer and the returned descriptions are geared to them rather than end users.
130 |Code 4: User error: These errors regard actions that the user has taken that are invalid, but not really errors in a traditional sense. The description field should be shown to users verbatim, in a clear and noticeable fashion. They are formatted as concise English sentences and end with appropriate punctuation marks.
133 |Code 5: Authorization error: This code represents an erroneous User/Auth header pair. This should trigger the user to provide correct credentials or fall back to anon mode.
136 |See also: the GitHub repository.
104 |BBJ is heavily inspired by image boards like 4chan, but it offers a simple 105 | account system to allow users to identify themselves and set profile 106 | attributes like a more traditional forum. Registration is optional and there 107 | are only minimal restrictions on anonymous participation.
108 |Being a command-line-oriented text board, BBJ has no avatars or file sharing 110 | capabilties, so its easier to administrate and can't be used to distribute illegal 111 | content like imageboards. It has very few dependancies and is easy to set up.
112 |The API is simple and doesn't use require complex authorization schemes or session management. 113 | It is fully documented on this site (though the verbage is still being revised for friendliness)
{{summary}}
4 |No results found
"); 64 | } 65 | 66 | if(jQuery){ 67 | /* 68 | * We currently only automatically hide bootstrap models. This 69 | * requires jQuery to work. 70 | */ 71 | jQuery('#mkdocs_search_modal a').click(function(){ 72 | jQuery('#mkdocs_search_modal').modal('hide'); 73 | }); 74 | } 75 | 76 | }; 77 | 78 | var search_input = document.getElementById('mkdocs-search-query'); 79 | 80 | var term = getSearchTerm(); 81 | if (term){ 82 | search_input.value = term; 83 | search(); 84 | } 85 | 86 | search_input.addEventListener("keyup", search); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /docs/site/mkdocs/js/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS text 2.0.12 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/requirejs/text for details 5 | */ 6 | /*jslint regexp: true */ 7 | /*global require, XMLHttpRequest, ActiveXObject, 8 | define, window, process, Packages, 9 | java, location, Components, FileUtils */ 10 | 11 | define(['module'], function (module) { 12 | 'use strict'; 13 | 14 | var text, fs, Cc, Ci, xpcIsWindows, 15 | progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 16 | xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, 17 | bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, 18 | hasLocation = typeof location !== 'undefined' && location.href, 19 | defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), 20 | defaultHostName = hasLocation && location.hostname, 21 | defaultPort = hasLocation && (location.port || undefined), 22 | buildMap = {}, 23 | masterConfig = (module.config && module.config()) || {}; 24 | 25 | text = { 26 | version: '2.0.12', 27 | 28 | strip: function (content) { 29 | //Strips declarations so that external SVG and XML 30 | //documents can be added to a document without worry. Also, if the string 31 | //is an HTML document, only the part inside the body tag is returned. 32 | if (content) { 33 | content = content.replace(xmlRegExp, ""); 34 | var matches = content.match(bodyRegExp); 35 | if (matches) { 36 | content = matches[1]; 37 | } 38 | } else { 39 | content = ""; 40 | } 41 | return content; 42 | }, 43 | 44 | jsEscape: function (content) { 45 | return content.replace(/(['\\])/g, '\\$1') 46 | .replace(/[\f]/g, "\\f") 47 | .replace(/[\b]/g, "\\b") 48 | .replace(/[\n]/g, "\\n") 49 | .replace(/[\t]/g, "\\t") 50 | .replace(/[\r]/g, "\\r") 51 | .replace(/[\u2028]/g, "\\u2028") 52 | .replace(/[\u2029]/g, "\\u2029"); 53 | }, 54 | 55 | createXhr: masterConfig.createXhr || function () { 56 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 57 | var xhr, i, progId; 58 | if (typeof XMLHttpRequest !== "undefined") { 59 | return new XMLHttpRequest(); 60 | } else if (typeof ActiveXObject !== "undefined") { 61 | for (i = 0; i < 3; i += 1) { 62 | progId = progIds[i]; 63 | try { 64 | xhr = new ActiveXObject(progId); 65 | } catch (e) {} 66 | 67 | if (xhr) { 68 | progIds = [progId]; // so faster next time 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return xhr; 75 | }, 76 | 77 | /** 78 | * Parses a resource name into its component parts. Resource names 79 | * look like: module/name.ext!strip, where the !strip part is 80 | * optional. 81 | * @param {String} name the resource name 82 | * @returns {Object} with properties "moduleName", "ext" and "strip" 83 | * where strip is a boolean. 84 | */ 85 | parseName: function (name) { 86 | var modName, ext, temp, 87 | strip = false, 88 | index = name.indexOf("."), 89 | isRelative = name.indexOf('./') === 0 || 90 | name.indexOf('../') === 0; 91 | 92 | if (index !== -1 && (!isRelative || index > 1)) { 93 | modName = name.substring(0, index); 94 | ext = name.substring(index + 1, name.length); 95 | } else { 96 | modName = name; 97 | } 98 | 99 | temp = ext || modName; 100 | index = temp.indexOf("!"); 101 | if (index !== -1) { 102 | //Pull off the strip arg. 103 | strip = temp.substring(index + 1) === "strip"; 104 | temp = temp.substring(0, index); 105 | if (ext) { 106 | ext = temp; 107 | } else { 108 | modName = temp; 109 | } 110 | } 111 | 112 | return { 113 | moduleName: modName, 114 | ext: ext, 115 | strip: strip 116 | }; 117 | }, 118 | 119 | xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, 120 | 121 | /** 122 | * Is an URL on another domain. Only works for browser use, returns 123 | * false in non-browser environments. Only used to know if an 124 | * optimized .js version of a text resource should be loaded 125 | * instead. 126 | * @param {String} url 127 | * @returns Boolean 128 | */ 129 | useXhr: function (url, protocol, hostname, port) { 130 | var uProtocol, uHostName, uPort, 131 | match = text.xdRegExp.exec(url); 132 | if (!match) { 133 | return true; 134 | } 135 | uProtocol = match[2]; 136 | uHostName = match[3]; 137 | 138 | uHostName = uHostName.split(':'); 139 | uPort = uHostName[1]; 140 | uHostName = uHostName[0]; 141 | 142 | return (!uProtocol || uProtocol === protocol) && 143 | (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && 144 | ((!uPort && !uHostName) || uPort === port); 145 | }, 146 | 147 | finishLoad: function (name, strip, content, onLoad) { 148 | content = strip ? text.strip(content) : content; 149 | if (masterConfig.isBuild) { 150 | buildMap[name] = content; 151 | } 152 | onLoad(content); 153 | }, 154 | 155 | load: function (name, req, onLoad, config) { 156 | //Name has format: some.module.filext!strip 157 | //The strip part is optional. 158 | //if strip is present, then that means only get the string contents 159 | //inside a body tag in an HTML string. For XML/SVG content it means 160 | //removing the declarations so the content can be inserted 161 | //into the current doc without problems. 162 | 163 | // Do not bother with the work if a build and text will 164 | // not be inlined. 165 | if (config && config.isBuild && !config.inlineText) { 166 | onLoad(); 167 | return; 168 | } 169 | 170 | masterConfig.isBuild = config && config.isBuild; 171 | 172 | var parsed = text.parseName(name), 173 | nonStripName = parsed.moduleName + 174 | (parsed.ext ? '.' + parsed.ext : ''), 175 | url = req.toUrl(nonStripName), 176 | useXhr = (masterConfig.useXhr) || 177 | text.useXhr; 178 | 179 | // Do not load if it is an empty: url 180 | if (url.indexOf('empty:') === 0) { 181 | onLoad(); 182 | return; 183 | } 184 | 185 | //Load the text. Use XHR if possible and in a browser. 186 | if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { 187 | text.get(url, function (content) { 188 | text.finishLoad(name, parsed.strip, content, onLoad); 189 | }, function (err) { 190 | if (onLoad.error) { 191 | onLoad.error(err); 192 | } 193 | }); 194 | } else { 195 | //Need to fetch the resource across domains. Assume 196 | //the resource has been optimized into a JS module. Fetch 197 | //by the module name + extension, but do not include the 198 | //!strip part to avoid file system issues. 199 | req([nonStripName], function (content) { 200 | text.finishLoad(parsed.moduleName + '.' + parsed.ext, 201 | parsed.strip, content, onLoad); 202 | }); 203 | } 204 | }, 205 | 206 | write: function (pluginName, moduleName, write, config) { 207 | if (buildMap.hasOwnProperty(moduleName)) { 208 | var content = text.jsEscape(buildMap[moduleName]); 209 | write.asModule(pluginName + "!" + moduleName, 210 | "define(function () { return '" + 211 | content + 212 | "';});\n"); 213 | } 214 | }, 215 | 216 | writeFile: function (pluginName, moduleName, req, write, config) { 217 | var parsed = text.parseName(moduleName), 218 | extPart = parsed.ext ? '.' + parsed.ext : '', 219 | nonStripName = parsed.moduleName + extPart, 220 | //Use a '.js' file name so that it indicates it is a 221 | //script that can be loaded across domains. 222 | fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; 223 | 224 | //Leverage own load() method to load plugin value, but only 225 | //write out values that do not have the strip argument, 226 | //to avoid any potential issues with ! in file names. 227 | text.load(nonStripName, req, function (value) { 228 | //Use own write() method to construct full module value. 229 | //But need to create shell that translates writeFile's 230 | //write() to the right interface. 231 | var textWrite = function (contents) { 232 | return write(fileName, contents); 233 | }; 234 | textWrite.asModule = function (moduleName, contents) { 235 | return write.asModule(moduleName, fileName, contents); 236 | }; 237 | 238 | text.write(pluginName, nonStripName, textWrite, config); 239 | }, config); 240 | } 241 | }; 242 | 243 | if (masterConfig.env === 'node' || (!masterConfig.env && 244 | typeof process !== "undefined" && 245 | process.versions && 246 | !!process.versions.node && 247 | !process.versions['node-webkit'])) { 248 | //Using special require.nodeRequire, something added by r.js. 249 | fs = require.nodeRequire('fs'); 250 | 251 | text.get = function (url, callback, errback) { 252 | try { 253 | var file = fs.readFileSync(url, 'utf8'); 254 | //Remove BOM (Byte Mark Order) from utf8 files if it is there. 255 | if (file.indexOf('\uFEFF') === 0) { 256 | file = file.substring(1); 257 | } 258 | callback(file); 259 | } catch (e) { 260 | if (errback) { 261 | errback(e); 262 | } 263 | } 264 | }; 265 | } else if (masterConfig.env === 'xhr' || (!masterConfig.env && 266 | text.createXhr())) { 267 | text.get = function (url, callback, errback, headers) { 268 | var xhr = text.createXhr(), header; 269 | xhr.open('GET', url, true); 270 | 271 | //Allow plugins direct access to xhr headers 272 | if (headers) { 273 | for (header in headers) { 274 | if (headers.hasOwnProperty(header)) { 275 | xhr.setRequestHeader(header.toLowerCase(), headers[header]); 276 | } 277 | } 278 | } 279 | 280 | //Allow overrides specified in config 281 | if (masterConfig.onXhr) { 282 | masterConfig.onXhr(xhr, url); 283 | } 284 | 285 | xhr.onreadystatechange = function (evt) { 286 | var status, err; 287 | //Do not explicitly handle errors, those should be 288 | //visible via console output in the browser. 289 | if (xhr.readyState === 4) { 290 | status = xhr.status || 0; 291 | if (status > 399 && status < 600) { 292 | //An http 4xx or 5xx error. Signal an error. 293 | err = new Error(url + ' HTTP status: ' + status); 294 | err.xhr = xhr; 295 | if (errback) { 296 | errback(err); 297 | } 298 | } else { 299 | callback(xhr.responseText); 300 | } 301 | 302 | if (masterConfig.onXhrComplete) { 303 | masterConfig.onXhrComplete(xhr, url); 304 | } 305 | } 306 | }; 307 | xhr.send(null); 308 | }; 309 | } else if (masterConfig.env === 'rhino' || (!masterConfig.env && 310 | typeof Packages !== 'undefined' && typeof java !== 'undefined')) { 311 | //Why Java, why is this so awkward? 312 | text.get = function (url, callback) { 313 | var stringBuffer, line, 314 | encoding = "utf-8", 315 | file = new java.io.File(url), 316 | lineSeparator = java.lang.System.getProperty("line.separator"), 317 | input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), 318 | content = ''; 319 | try { 320 | stringBuffer = new java.lang.StringBuffer(); 321 | line = input.readLine(); 322 | 323 | // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 324 | // http://www.unicode.org/faq/utf_bom.html 325 | 326 | // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: 327 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 328 | if (line && line.length() && line.charAt(0) === 0xfeff) { 329 | // Eat the BOM, since we've already found the encoding on this file, 330 | // and we plan to concatenating this buffer with others; the BOM should 331 | // only appear at the top of a file. 332 | line = line.substring(1); 333 | } 334 | 335 | if (line !== null) { 336 | stringBuffer.append(line); 337 | } 338 | 339 | while ((line = input.readLine()) !== null) { 340 | stringBuffer.append(lineSeparator); 341 | stringBuffer.append(line); 342 | } 343 | //Make sure we return a JavaScript string and not a Java string. 344 | content = String(stringBuffer.toString()); //String 345 | } finally { 346 | input.close(); 347 | } 348 | callback(content); 349 | }; 350 | } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && 351 | typeof Components !== 'undefined' && Components.classes && 352 | Components.interfaces)) { 353 | //Avert your gaze! 354 | Cc = Components.classes; 355 | Ci = Components.interfaces; 356 | Components.utils['import']('resource://gre/modules/FileUtils.jsm'); 357 | xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc); 358 | 359 | text.get = function (url, callback) { 360 | var inStream, convertStream, fileObj, 361 | readData = {}; 362 | 363 | if (xpcIsWindows) { 364 | url = url.replace(/\//g, '\\'); 365 | } 366 | 367 | fileObj = new FileUtils.File(url); 368 | 369 | //XPCOM, you so crazy 370 | try { 371 | inStream = Cc['@mozilla.org/network/file-input-stream;1'] 372 | .createInstance(Ci.nsIFileInputStream); 373 | inStream.init(fileObj, 1, 0, false); 374 | 375 | convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] 376 | .createInstance(Ci.nsIConverterInputStream); 377 | convertStream.init(inStream, "utf-8", inStream.available(), 378 | Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); 379 | 380 | convertStream.readString(inStream.available(), readData); 381 | convertStream.close(); 382 | inStream.close(); 383 | callback(readData.value); 384 | } catch (e) { 385 | throw new Error((fileObj && fileObj.path || '') + ': ' + e); 386 | } 387 | }; 388 | } 389 | return text; 390 | }); 391 | -------------------------------------------------------------------------------- /docs/site/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 |The server has an endpoint called db_validate
. What this does is take
101 | a key
and a value
argument, and compares value
to a set of rules specified by
102 | key
. This is the same function used internally by the database to check
103 | values before committing them to the database. By default it returns a
104 | descriptive object under data
, but you can specify the key/value pair
105 | "error": True
to get a standard error response back. A standard call
106 | to db_validate
will look like this.
{
108 | "key": "title",
109 | "value": "this title\nis bad \nbecause it contains \nnewlines"
110 | }
111 |
112 |
113 | and the server will respond like this when the input should be corrected.
114 |{
115 | "data": {
116 | "bool": False,
117 | "description": "Titles cannot contain whitespace characters besides spaces."
118 | },
119 | "error": False,
120 | "usermap": {}
121 | }
122 |
123 |
124 | if everything is okay, the data object will look like this instead.
125 | "data": {
126 | "bool": True,
127 | "description": null
128 | },
129 |
130 |
131 | Alternatively, you can supply "error": True
in the request.
{
133 | "error": True,
134 | "key": "title",
135 | "value": "this title\nis bad \nbecause it contains \nnewlines"
136 | }
137 | // and you get...
138 | {
139 | "data": null,
140 | "usermap": {},
141 | "error": {
142 | "code": 4,
143 | "description": "Titles cannot contain whitespace characters besides spaces."
144 | }
145 | }
146 |
147 |
148 | The following keys are currently available.
149 |The descriptions returned are friendly, descriptive, and should be shown 159 | directly to users
160 |By using this endpoint, you will never have to validate values in your 161 | own code before sending them to the server. This means you can do things 162 | like implement an interactive prompt which will not allow the user to 163 | submit it unless the value is correct.
164 |This is used in the elisp client when registering users and for the thread 165 | title prompt which is shown before opening a composure window. The reason 166 | for rejection is displayed clearly to the user and input window is restored.
167 |(defun bbj-sane-value (prompt key)
168 | "Opens an input loop with the user, where the response is
169 | passed to the server to check it for validity before the
170 | user is allowed to continue. Will recurse until the input
171 | is valid, then it is returned."
172 | (let* ((value (read-from-minibuffer prompt))
173 | (response (bbj-request! 'db_validate 'value value 'key key)))
174 | (if (alist-get 'bool response)
175 | value ;; return the user's input back to the caller
176 | (message (alist-get 'description response))
177 | (sit-for 2)
178 | (bbj-sane-value prompt key))))
179 |