Skip to content

Add a command

This page walks through adding a WebSocket command to originchats-osl: planning the request and response, writing the handler, registering it, and testing it. Read Architecture first for how a command flows through the server.

Before writing code, decide:

  • the command name, in snake_case
  • the request fields, their types and defaults, and which are required
  • the direct response, and any event other clients receive
  • the permission the command needs, and any per-resource rule (such as channel visibility or ownership)
  • what it saves, and how it rolls back if the save fails
  • whether clients need to discover it through the handshake

Use one command per operation. A partial-update command can take several optional fields, but it should reject a request that sets none of them.

Create src/api/handlers/<group>/<command>.osl. The file name matches the command, the function is handle plus the command in PascalCase, and error frames use the command as src. This is webhook_create, trimmed slightly:

*schema.Schema schemaWebhookCreate = schema.object({
channel: schema.string().trim().minLen(1),
name: schema.string().trim().minLen(1)
})
def handleWebhookCreate(*ws.Connection conn, object msg) object[] (
string command = "webhook_create"
auto err = (string text) object[] -> objErr(text, command)
object data = schemaWebhookCreate.safeParse(msg) catch (
return err(_.unwrapErr().message)
)
object channelInfo = channelGet(data.channel).assertElse(object, {})
if channelInfo.len == 0 (
return err("Channel not found")
)
object webhook = webhookCreate(data.channel, data.name, conn.Get("userid"))
if webhook.error != null (
return err(`Failed to create webhook '${data.name}' in channel '${data.channel}'`)
)
webhookAudit(conn, command, webhook, {channel: data.channel})
return [{cmd: command, webhook}]
)

Rules for handlers:

  • Take the connection and the decoded request, and return object[].
  • Return bad input as an error frame with objErr. Don’t throw for user mistakes. A thrown value also becomes an error frame, but its text is whatever was thrown.
  • Validate with a std:schema schema first, then pass only validated values on. For partial updates, list the allowed keys so unknown fields can’t reach storage.
  • Don’t check login or the command-level permission in the handler. The router does that. See Set permissions.

If the group directory is new, add import "handlers/<group>" to the list at the top of src/api/index.osl. Importing a directory brings in the handler files inside it.

The handler adapts the protocol. Shared rules belong elsewhere:

Layer Owns
src/api/helpers/<area>/ Permission and validation rules, serialization, and behavior shared between handlers or with slash commands.
src/db/<resource>/ File formats, locks, saving, and rollback.
src/utils/ Response builders and broadcast helpers.
src/api/index.osl Parsing, access checks, dispatch, listeners, and broadcast routing.

Don’t add a helper that only wraps one call. Keep single-use logic in the handler unless it clearly belongs to a storage module.

Command-level rules live in tables in src/api/access.osl:

Table Add the command when
COMMAND_PERMISSIONS The whole command needs one permission, such as manage_server.
COMMAND_ALTERNATIVES Any one of several permissions is enough.
TIMEOUT_BLOCKED A timed-out user shouldn’t be able to run it. Include every command that writes or acts.
METADATA_REFRESH_COMMANDS It can change what other users see about roles or permissions.
UNAUTHENTICATED_COMMANDS It must work before login. This is rare.

The owner and anyone with administrator pass every permission check. Only the literal owner role bypasses channel permission lists.

Per-resource rules stay in the helper for that resource: channel visibility (canUserView), editing your own message versus anyone’s, thread ownership, and role hierarchy (roleManageError).

A mutation follows this order:

validate -> authorise -> normalise -> change and save -> audit or refresh -> respond

Keep the previous in-memory value until the save succeeds. If it fails, put the old value back and return an error. Never broadcast a change that wasn’t saved.

Use the resource’s storage functions and the transaction helper (dbCommit) rather than writing files directly. See Persistence.

Admin actions should call auditRecord. Channel changes should call refreshChannels, which sends each connection channel_get or channels_get depending on what its client supports.

Add one case to dispatchCmd in src/api/index.osl:

"webhook_create" -> handleWebhookCreate(conn, msg)

An unknown command returns no frames, on purpose.

If clients need to discover the command, add its name to handshakeCapabilities() in src/api/handlers/auth/handshake.osl. Add it conditionally if the feature can be turned off, as server_side_embeds is.

Mark a returned frame global: true when other connections should receive it. Include channel so the router only sends it to users who can see that channel and aren’t severed from the caller.

The router sends the caller its copy with the listener, then removes the listener and sends the frame to everyone else who qualifies. Don’t also send it yourself.

For events every logged-in user may see, use broadcastAuthed. Presence needs per-viewer filtering. Keep sensitive admin results direct.

Add or extend a *.test.osl file next to the code, and run it with osl test. See Tests. If the command belongs in one of the access.osl tables, extend src/api/access.test.osl, which checks those tables.

Then try it against a real server built from your branch, and cover:

  • the smallest valid request
  • each validation error a client might hit
  • a user without the permission, a user who has left, and a timed-out user
  • resources the user can’t see or doesn’t own
  • a failed save, and that memory is rolled back
  • the listener on the direct response, and no listener on broadcasts
  • who receives the broadcast
  • reconnecting, and clients that don’t advertise the capability

Add a page under src/content/docs/protocol/commands/ in the docs repository. Read the handler and every helper it calls, then document the request, response, broadcasts, permissions, and errors, quoting error text exactly. Write the page by hand; don’t generate it from source.

Update the command index and any concept or client guide the change affects, then build the docs.