This is the codebase for castling.club, an ActivityPub server with a single hardcoded 'King' service actor that acts as a chess arbiter.
This code is MIT-licensed, see LICENSE.md
.
Not a requirement, but if you use this code for anything, I'd love to hear about it! Send me a toot: @[email protected]
To run the app, you need Node.js (at least 16.x), Yarn, and PostgreSQL (at least 9.6).
First, install the dependencies and build the TypeScript code:
yarn install
yarn build
npm install dotenv
Next, you should configure the app.
Configuration is provided through environment variables, which can also be
provided in a .env
file. The defaults should work for development. The
following variables are used (with their defaults):
-
APP_SCHEME="http"
The scheme used to access the app. The app only talks plain HTTP, but this should be set to
https
when behind a reverse proxy providing HTTPS. -
APP_DOMAIN="localhost:5080"
The domain used to access the app. You should set up a virtual host matching this, and have it proxy to the app.
-
APP_ADMIN_URL=""
Profile URL of the server admin, publicly accessible through the NodeInfo API.
-
APP_ADMIN_EMAIL=""
Email of the server admin, publicly accessible through the NodeInfo API.
-
APP_KEY_FILE="signing-key"
Filename of the RSA private key (in PEM format) used to sign activities for the 'King' actor. A matching file with a
.pub
suffix MUST be present, containing the RSA public key (also in PEM format).Use
tools/gen-signing-key.sh <filename>
to generate a pair. -
APP_HMAC_SECRET="INSECURE"
The secret used to sign public data generated by the app that must later be verified. The default MUST NOT ever be used in a public instance.
Currently, this is only used to sign image URLs, so that they cannot be tampered with.
One can be generated with, for example:
head -c 63 /dev/random | base64
-
NODE_ENV="development"
One of
development
orproduction
.It's important to set this to
production
on a live instance. When not in production, the app will not enforce, but only warn about security issues. (E.g. invalid request signatures, and federation over non-HTTPS.)This variable may also change various behaviour in library dependencies.
-
PORT="5080"
The TCP port the app will listen on for HTTP requests.
The app will always bind on all interfaces, and MUST be properly firewalled in a public instance.
-
PostgreSQL connection variables follow the standard set defined at: https://www.postgresql.org/docs/9.6/static/libpq-envars.html
Now generate the signing key:
./tools/gen-signing-key.sh signing-key
Make sure the database exists in PostgreSQL, then create the schema:
yarn migrate up
The app can now be started with:
yarn start
Debug logging is provided by the debug module. For development, castling.club's own debug logging can be enabled using, for example:
DEBUG='chess:*' yarn start
With the app running locally in development using default settings, you can run an automated test that plays a short game:
./functional-test/main.mjs
The server.js
entry point loads configuration, then starts the two main tasks
of the app:
src/front/
contains the frontend code handling HTTP requests.src/deliver/
contains the outbox delivery queue processing code.
These could easily be split into separate processes, and could also individually be scaled horizontally, because all state lives in PostgreSQL. In practice, that kind of scale is not reached, so things are kept simple and single-process for now.
The codebase heavily uses async/await, with the Koa framework for its frontend.
Instances of library dependencies and our own services (such as Koa, the
PostgreSQL client, etc.) live on an app
object. This structure can be best
seen in src/shared/createApp.js
and the index.js
files of each task
directory. This is basically poor-man's dependency injection; there's no
automated system to resolve the dependencies between objects, we just create
them in the order we know works.
The castling.club codebase sticks to ActivityPub naming. Notably, this means: not 'toots', but 'notes'.
When interacting with the bot, there are roughly 6 steps that take place:
- The user looks up the bot by name.
- The user publishes a note.
- The user's instance delivers it to the bot's and others' instances.
- The bot acts on the note and publishes a reply.
- We deliver the reply to the users' instances.
- The users views our reply.
Step 1 involves an optional Webfinger request, and fetching our Actor object.
Both of these are implemented in src/front/actor.js
. In a server
implementation like Mastodon, these would be dynamic, because there's an account
system. But in castling.club, a single account is simply hardcoded.
Step 3 is when we receive a signed request POST /inbox
. This is implemented in
src/front/inbox.js
. The signature is verified by code in
src/shared/signing.js
, then dispatched as an internal event noteCreated
.
Step 4 starts when the event is picked up in src/front/dispatch.js
. Here, the
note is parsed and methods are called on src/front/game.js
and
src/front/challengeBoard.js
, which are roughly our controllers when thinking
in MVC. When these decide a reply needs to be sent, they call createObject()
of src/front/outbox.js
, which queues deliveries to user inboxes.
Step 5 is where outbox deliveries are picked up, implemented in
src/deliver/deliver.js
. Each delivery is a two-step process, where first the
addressed Actor is fetched to discover the inbox, then the note is actually
delivered to the inbox. These are implemented as essentially two separate jobs
folded into a single table deliveries
. Keeping these separate allows combining
deliveries that have the same shared inbox.
Step 6 is of note, because images are lazily rendered. A signed image URL is
generated in step 4, and when requested, the image is fetched from cache or
rendered on the spot. This code lives in src/front/draw.js
.
The ActivityPub objects are all accessible by browser as HTML. This is
important, because the actor URL and note URLs are often visible to the user.
Castling.club renders templates for these when accessed through a browser, as
well as providing various other browser-only pages in src/front/misc.js
. The
actual template files are EJS files, found in assets/tmpl/
.