• Stars
    star
    114
  • Rank 306,368 (Top 7 %)
  • Language
    Haskell
  • License
    MIT License
  • Created about 10 years ago
  • Updated over 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

It's kinda like a forum.

Basilica

You can see a live demo running at https://basilica.horse, which currently hosts both the API and the official Om client.

A basilica is like a forum, but for a few ill-defined differences. For more detail please consult the table below, adapted from a crude sketch I made while drunk.

Forum Basilica
PHP Haskell
90s 2010s
trolls friends
"rich formatting" markdown
paging lazy tree
threads ↑ comments ↓ uniform hierarchy
<form> HTTP API
inline CSS bots, webhooks, extensions
F5 websockets

Status

Basilica is usable. It is not a comprehensive, beautiful piece of software, but it works and the canonical instance of it has been live and running since 2014.

Further development is not very likely, since it works well enough for my purposes.

API

Resources

Basilica defines a few resources, which are always communicated in JSON.

Sometimes the API will send resolved data, which means that it will turn:

"idResource": 123

Into:

"resource": { "id": 123, ... }

When it does so will be documented in the route response.

Unless otherwise specified, no value will be null.

Post

{ "id": 49
, "idParent": 14
, "idUser": 43
, "at": "2014-08-17T01:19:15.139Z"
, "count": 0
, "content": "any string"
, "children": []
}
  • id is a monotonically increasing identifier, and it is the only field that should be used for sorting posts.
  • idParent can be null. Root posts have no parents.
  • idUser is the id of the user who created the post.
  • at is a string representing the date that the post was created, in ISO 8601 format. This field exists to be displayed to the user; it should not be used for sorting or paging. Use id for that.
  • count is the total number of children that this post has, regardless of the number of children returned in any response.
  • children is a list of posts whose idParent is equal to this post's id. This is not necessarily an exhaustive list. Comparing the number of elements in this field to the count field can tell you if there are more children to load.
    • children will always be sorted by id, with newer posts (larger ids) in the front of the list

User

{ "id": 32
, "email": "[email protected]"
, "name": "ian"
, "face": {}
}
  • email will be omitted unless otherwise specified in the route documentation
  • face is an object that indicates how to render a thumbnail of the user. Currently the only valid options are:
    • { "gravatar": "a130ced3f36ffd4604f4dae04b2b3bcd" }
    • { "string": "☃" }
      • not implemented

Code

Codes are never communicated via JSON, so it doesn't make sense to show their format. Publicly, they can be considered strings. They happen to currently be hexadecimal strings, but that's an implementation detail that may change.

Token

{ "id": 91
, "token": "a long string"
, "idUser": 32
}

Authentication

There's a goofy hand-rolled auth scheme.

There are no passwords. Authentication is done purely through email. The process looks this:

  • request a code (see POST /codes)
  • Basilica emails it to you
  • you trade in the code for a token (see POST /tokens)
  • you use that token to authenticate all future requests (by setting the X-Token header)

I'm gonna repeat that last thing because it's important: you need to set an X-Token header to make an authenticated request. No cookies, query parameters, nothing like that. That header is the only thing that counts.

This is similar to the "forgot my password" flow found in most apps, except that you don't have to pretend to remember anything.

Routes

Postal Routes

POST /posts/:idParent

  • requires a valid token
  • for: creating a new post as a child of the specified idParent
  • idParent is optional. If omitted, this will create a post with idParent set to null.
  • arguments: an x-www-form-urlencoded body is expected with
    • content (any string)
      • required
      • must not be the empty string
  • response: the newly created post, JSON-encoded
    • idUser will be resolved
    • if the post has a count other than 0, that's a bug
    • the post will not have children
$ curl -i                             # show response headers (otherwise a 401 is very confusing)
       -X POST                        # set the HTTP verb
       --data "content=hello%20world" # escape your string!
       -H "X-Token: asdf"             # requires authentication
       "http://localhost:3000/posts"  # the actual route

GET /posts/:id

  • for: loading posts and post children
  • arguments: query parameters
    • depth: how deeply to recursively load children
      • not implemented
      • default: 1
      • if 0, the response will not include children at all
      • valid values: just 0 and 1 right now
    • after: the id of a post
      • not implemented
      • optional
      • ignored if depth is 0
      • the response will not include any posts created before this in the children list (recursively, if multiple depths are ever supported)
    • limit: the maximum number of children to load
      • not implemented
      • default: 50
      • ignored if depth is 0
      • valid values: 1 to 500
      • applies recursively, if multiple depths are ever supported
  • response: a JSON-encoded post
    • if depth is greater than 0, it will include children
    • idUser will be resolved for the root post and all children, recursively
    • remember that count is always the total number of children, regardless of the limit

GET /posts

  • for: loading every single post in the entire database, catching up after a disconnect (with after)
  • arguments: query parameters
    • after: the id of a post
      • optional
      • the response will only contain posts created after the specified post
    • before: the id of a post
      • optional
      • the response will only contain posts created before the specified post
    • limit: the maximum number of posts to return
      • default: 200
      • valid values: 1 to 500
  • response:
    • if after is specified, and there were more than limit posts to return, this returns... some error code. I'm not sure what though. 410, maybe?
      • not implemented
    • otherwise, a JSON array of posts with no children fields, sorted by id from newest to oldest
    • idUser will be resolved

User Routes

POST /users

  • for: signing up for a new account
  • arguments: x-www-form-urlencoded
    • email: the email address for the user.
    • name: the username. Must contain only alphanumeric characters.
  • response:
    • 200 with the newly created user
    • 400 if the username contains non-alphanumeric characters
    • 409 if an account already exists with the specified username or email address, with no response body
  • side effect: automatically invokes POST /codes with the given email address

Auth Routes

POST /codes

  • for: creating a new code, which can be used to obtain a token
  • arguments: x-www-form-urlencoded
    • email: the email address of the user for which you would like to create a code
  • response: this route will always return an empty response body with a 200 status code, regardless of whether email corresponds to a valid email address
    • a timing attack can absolutely be used to determine if the email corresponds to a valid account or not; knock yourself out
  • side effect: if the email address specified matches a user account, Basilica will send an email containing the newly created code.

DELETE /codes/:code

  • for: revoking a code, in case it was sent in error
  • not implemented
  • or documented

POST /tokens

  • for: creating a new token
  • arguments: x-www-form-urlencoded
    • code: a code obtained from a call to POST /codes
      • required
  • note: auth tokens don't do anything yet
  • response:
    • if the code is valid, a JSON-encoded token with idUser resolved into user
    • otherwise, 401
  • side effect: invalidates the code specified

GET /tokens

  • for: listing tokens
  • response: an array of JSON-encoded token objects with only id specified
    • probably other stuff later
  • not implemented

DELETE /tokens/:id

  • for: revoking a token ("logging out")
  • arguments:
    • id: the id of the token to revoke
      • required
  • response: 200, 404, or 401
  • not implemented

Websockets

There is currently one websocket route, a single firehose stream of all new posts created, in JSON, with idUser resolved. The route is just /, with the ws or wss protocol.

When connected, Basilica will periodically send ping frames. If the client doesn't respond in a timely manner, that client will be closed with either a friendly or slightly hostile message.

Currently this is set to ping every 20 seconds and to disconnect clients if more than 40 seconds passes without receiving a pong. Don't rely on those values, though. Just pong the pings as quickly as you can. All websocket libraries should do this for you automatically.

Client Implementation Notes

  • When a new post is created, clients should update their cached count value for its parent. It's important that this value stays up-to-date for accurate paging.
  • When a disconnect occurs, and it will, reconnect the socket and then call GET /posts?after=id, where id is the latest post that you knew about. It's important that you reconnect the socket before filling the gap, otherwise any post created in the brief moment after the response and before the socket comes back will be lost.

Basiliclients

Development

Basilica uses SQLite. You need to create the database.

$ sqlite3 basilica.db ".read schema.sql"

Basilica is developed using stack:

$ stack build

After that you can modify the conf file. Here's a list of all keys and their meanings:

port = 3000
dbpath = "basilica.db"
client-origin = "http://localhost:3333"
client-url = "http://localhost:3333/client/"
mailgun-key = "asdf"
  • port is the port that the HTTP and WS server will run on.
  • dbpath is the path to the SQLite file that you've initialized with the schema.
  • client-origin is optional. When specified, it will set the Access-Control-Allow-Origin header and respond to OPTIONS requests appropriately. This is especially useful for development when you might be serving the client from a different local port.
  • client-url is used in emails to generate one-click login links.
  • mailgun-key is the API key for the Mailgun account you want to use to send emails. If omitted, codes will be written to stdout.

Then you can run it.

$ stack exec basilica

Now you're ready to basilicate.

Contributors

  • Ian started it
  • Hao made websockets shinier

More Repositories

1

sd

a cozy nest for your scripts
Shell
704
star
2

bauble

a playground for making 3D art with lisp and math
Janet
378
star
3

zsh-autoquoter

automatically quote arguments to commands like `git commit -m`
Shell
114
star
4

mixologician

optimize your home bar with ✨logic programming✨
Prolog
75
star
5

judge

self-modifying test library for janet
Janet
68
star
6

toodle.studio

turtle graphics playground
TypeScript
66
star
7

dotfiles

My dotfiles. For easy clonin'.
Emacs Lisp
49
star
8

macaroni

macro spaghetti code
Janet
28
star
9

ocamlsyntax.com

How do you do that recursive GADT thing again?
CSS
27
star
10

cmd

command-line argument parser for Janet
Janet
24
star
11

jimmy

Janet bindings for the persistent data structure library "immer"
C++
18
star
12

tightrope

Making Slackbots is hard! But wait: now it's easy.
Haskell
14
star
13

janet-stuff

an index of all of my public-facing janet projects
11
star
14

pat

a better pattern matching macro for janet
Janet
10
star
15

sd-nix

some helpers for working with nix
Shell
10
star
16

privy

inline output for the ivy array language
Janet
9
star
17

effing.js

Function functions.
CoffeeScript
9
star
18

bytemap

text-based canvas
Janet
9
star
19

basilica-client

An Om client for basilica
Clojure
8
star
20

janet-cross-compile

demonstration of using zig cc to cross-compile native janet binaries
C
8
star
21

codemirror-lang-janet

Janet support for CodeMirror 6
TypeScript
7
star
22

memegen

A meme generator for discriminating meme enthusiasts
Haskell
7
star
23

banquet

fancy tables
Janet
7
star
24

aoc-2023

advent of code 2023 in janet
Janet
6
star
25

square

alternate PEG syntax for Janet
Janet
5
star
26

petal

my pet array language
Rust
5
star
27

janet-midi

Janet bindings for RtMidi, a cross-platform MIDI library
C++
5
star
28

janet-module

a simple module macro
Janet
5
star
29

utf8-parser

A fun way to learn about UTF-8 and parser combinators
Haskell
5
star
30

janet-clipper

Janet bindings for clipper, a polygon utility library
C++
3
star
31

to-do

a very simple plain-text todo list
Janet
3
star
32

zsh-expander

predictable fzf-based completion for zsh
Shell
3
star
33

cmp

comparison combinators for Janet
Janet
3
star
34

rogue

A roguelike, in Haskell
Haskell
3
star
35

janet-whereami

a janet wrapper around the whereami library
C
2
star
36

httprintf

Some scripts for creating a development HTTP server.
Shell
2
star
37

matchbook

Declarative pattern matching for JavaScript
CoffeeScript
2
star
38

heap

comparator-based min-max heap for janet
Janet
2
star
39

steno

command-line snapshot testing tool
Janet
2
star
40

spoon

command line interface for lambda soup
OCaml
2
star
41

territory

An abstract game of territory control made in a single sitting
Objective-C
1
star
42

nix-zshell

Hack to make nix-shell play nicely with zsh
Shell
1
star
43

brinks

Algebraic data types for JavaScript
JavaScript
1
star
44

privy-mode

emacs major mode for the ivy array language and privy preprocessor
Emacs Lisp
1
star
45

overtone-demo

A demo of overtone. For a tech talk.
Clojure
1
star
46

dismissive-clojure

Send emails... to the future!
Clojure
1
star
47

ctrl-c

Being interrupted as a service
Shell
1
star
48

inline

Inline: offline outlining.
JavaScript
1
star
49

rundown

running with markdown
Rust
1
star
50

ianthehenry

1
star
51

dismissive

Emailing the future as a service
Haskell
1
star
52

tales

An adventure in collaborative storytelling.
Haskell
1
star