decent

Decent API Specification 0.1.0

Communicating with the API

Misc


Authenticating with the API

Session IDs

When a request is made to the API, the server searches for a session ID given in the request using:

Endpoints labeled requires session will error if no session or an invalid session is provided. requires admin session means that the session’s user must be an admin.

Authorization

Authorization is a simple form of privacy which prevents clients from interacting with the server API without being authorized to do so (usually manually, by a human). This limits interaction to specific users, which may be wanted so that the server is “private”.

It should be noted that enabling authorization does not encrypt messages or user data; it simply limits who can access that data via the API.

Authorization is a server property and can only be enabled via the command line:

> set requireAuthorization on|off

This will cause all endpoints except those marked never requires session to require authentication.


Terminology

Dates

In this document, “dates” are integers specified according to JavaScript’s Date.now function. This is equal to the number of milliseconds elapsed since the UNIX epoch.

Programming languages which expect a UNIX timestamp may stumble as they expect a number of seconds since the UNIX epoch, not a number of milliseconds.

Names

Several parts of the API expect names (Name) to be given. These names will eventually be displayed to users, and so must follow a basic guideline for being formatted.

Names may consist only of alphanumeric characters, underscores (_), and dashes (-). When a name which does not follow these guidelines is given to an endpoint, an INVALID_NAME error will be returned and the request will have no action.

Errors

Nearly all HTTP endpoints return errors situationally. Generally, when the processing of a request errors, its response will have the error property, which will follow the form { code, message }.

The message property is a string of a human-readable English message briefly explaining what went wrong, and the code is a permanent identifier string for the type of error that happened. Checking code is useful for displaying custom messages or behavior when particular errors occur.

The following list describes each possible error code:


HTTP Endpoints

All endpoints respond in JSON, and those which take POST bodies expect it to be formatted using JSON.

Retrieve server version [GET /api]

Returns { decentVersion }. Should be used to check to see if a particular server is compatible with this spec. Note that Decent follows SemVer, so unless the MAJOR (first) portion of the version number is different to what you expect communication should work fine.

GET /api/

<- {
<-   "decentVersion": "0.1.0"
<- }

Settings

Model:

{
  "name": string,
  "authorizationMessage": string
}

Retrieve all settings [GET /api/settings]

Returns { settings }, where settings is an object representing server-specific settings.

GET /api/settings

<- {
<-   "settings": {
<-     "name": "Unnamed Decent chat server",
<-     "authorizationMessage": "Unauthorized - contact this server's webmaster to authorize your account for interacting with the server."
<-   }
<- }

Modify settings [POST /api/settings]

Returns { results } if successful, where results is an object describing the result of each changed setting. Updates settings with new values provided.

POST /api/settings

-> {
->   "name": "My Server"
-> }

<- {
<-   "result": {
<-     "name": "updated"
<-   }
<- }

Properties

Properties can only be modified on the command line.

Model:

{
  // If true, always use HTTPS to access the server.
  "useSecure": boolean,

  // If true, authorization is enabled. This means almost all endpoints
  // expect a session ID to be provided!
  "useAuthorization": boolean
}

Retrieve all properties [GET /api/properties]

Returns { properties }, where properties is an object representing server-specific properties.

GET /api/properties

<- {
<-   "properties": {
<-     "useSecure": false,
<-     "useAuthorization": false
<-   }
<- }

Upload an image [POST /api/upload-image]

Returns { path }, where path is a relative URL to the uploaded image file.

POST /api/upload-image

-> (form data)

<- {
<-   "path": "/uploads/1234/image.png"
<- }

This endpoint may return an error, namely UPLOAD_FAILED or UPLOADS_DISABLED.


Emotes

Model:

{
  "shortcode": Name, // Without colons
  "imageURL": string
}

Related events:

List emotes [GET /api/emotes]

Returns { emotes }, where emotes is an array of emote objects.

GET /api/emotes

<- {
<-   "emotes": []
<- }

Add a new emote [POST /api/emotes]

Returns {} if successful. Emits emote/new.

POST /api/emotes

-> {
->   "imageURL": "https://example.com/path/to/emote.png",
->   "shortcode": "package"
-> }

<- {}

View an emote [GET /api/emotes/:shortcode]

302 redirects to the imageURL of the emote specified. 404s if not found or invalid.

<!-- To view the :package: emoji in HTML: -->
<img src='/api/emotes/package' width='16' height='16'/>

Delete an existing emote [DELETE /api/emotes/:shortcode]

Returns {} if successful. Emits emote/delete.

DELETE /api/emotes/package

<- {}

Sessions

Model:

{
  "id": string,
  "dateCreated": number
}

Fetch the current user’s sessions [GET /api/sessions]

Responds with { sessions }, where sessions is an array of sessions that also represent the user that the provided session represents (the callee; you).

GET /api/sessions

<- {
<-   "sessions": [
<-     {
<-       "id": "12345678-ABCDEFGH",
<-       "dateCreated": 123456789000
<-     }
<-   ]
<- }

Login [POST /api/sessions]

Responds with { sessionID } if successful, where sessionID is the ID of the newly-created session. Related endpoint: register.

POST /api/sessions

-> {
->   "username": "admin",
->   "password": "abcdef"
-> }

<- {
<-   "sessionID": "12345678-ABCDEFGH"
<- }

Fetch session details [GET /api/sessions/:id]

Responds with { session, user } upon success, where session is a session and user is the user this session represents.

GET /api/sessions/12345678-ABCDEFGH

<- {
<-   "session": {
<-     "id": "12345678-ABCDEFGH",
<-     "dateCreated": 123456789000
<-   },
<-   "user": {
<-     "id": "1234",
<-     "username": "admin",
<-     // ...
<-   }
<- }

Logout [DELETE /api/sessions/:id]

Responds with {} upon success. Any further requests using the provided session ID will fail.

DELETE /api/sessions/12345678-ABCDEFGH

<- {}

Messages

Model:

{
  "id": ID,
  "channelID": ID,

  // The content of the message
  "text": string,

  // The author's details, at the time of creation
  "authorID": ID,
  "authorUsername": Name,
  "authorAvatarURL": string,

  "dateCreated": number,
  "dateEdited": number | null,

  "reactions": [ Reaction ],
  "mentionedUserIDs": [ ID ]
}

Note that message mentions live in the message content (text) as <@USER_ID>, where USER_ID is the ID of the user that is being mentioned; these appear in mentionedUserIDs of messages for ease of access.

Related events:

Send a message [POST /api/messages]

On success, emits message/new and returns { messageID }. Also marks channelID as read for the author.

POST /api/messages

-> {
->   "channelID": "5678",
->   "text": "Hello, world!"
-> }

<- {
<-   "messageID": "1234"
<- }

Retrieve a message [GET /api/messages/:id]

Returns { message } where message is a message object.

GET /api/messages/1234

<- {
<-   "message": {
<-     "id": "1234",
<-     // ...
<-   }
<- }

Edit a message [PATCH /api/messages/:id]

Emits message/edit and returns {}.

PATCH /api/messages/1234

-> {
->   "text": "Updated message text"
-> }

<- {}

This endpoint will return a NOT_YOURS error if you do not own the message in question.

Delete a message [DELETE /api/messages/:id]

Emits message/delete and returns {}.

DELETE /api/messages/1234

<- {}

This endpoint may return a NOT_YOURS error if you do not own the message in question. Note that admins may delete any message.


Channels

Model:

{
  "id": ID,
  "name": string // Does not include a hash
}

Extra data

This data is only present if a valid, logged-in session ID is provided to channel-returning endpoints.

{
  // Number of 'unread' messages, capped at 200. Unread messages are
  // simply messages that were sent more recently than the last time
  // the channel was marked read by this user.
  "unreadMessageCount": number,

  "oldestUnreadMessageID": ID | null,
}

Related events:

Get list of channels [GET /api/channels]

Returns { channels }, where channels is an array of channels. Note unreadMessageCount will only be returned if this endpoint receives a session.

GET /api/channels

<- {
<-   "channels": [
<-     {
<-       "id": "5678",
<-       "name": "general"
<-     }
<-   ]
<- }

Create a channel [POST /api/channels]

On success, emits channel/new and returns { channelID }.

POST /api/channels

-> {
->   "name": "general"
-> }

<- {
<-   "channelID": "5678"
<- }

May return an error: MUST_BE_ADMIN, NAME_ALREADY_TAKEN, INVALID_NAME.

Retrieve a channel [GET /api/channels/:id]

Returns { channel }. Note extra data will only be returned if this endpoint receives a logged-in session ID.

GET /api/channels/5678

<- {
<-   "id": "5678",
<-   "name": "general"
<- }

May return an error, including MUST_BE_ADMIN, NAME_ALREADY_TAKEN, and INVALID_NAME.

Rename a channel [PATCH /api/channels/:id]

Returns {} if successful, emitting channel/update.

PATCH /api/channels/5678

-> {
->   "name": "best-channel"
-> }

<- {}

Delete a channel [DELETE /api/channels/:id]

Returns {} if successful. Emits channel/delete.

DELETE /api/channels/5678

<- {}

Mark a channel as read [POST /api/channels/:id/mark-read]

Marks the channel as read (ie. sets unreadMessageCount to 0), returning {}. Emits channel/update including extra data if this socket is authenticated.

POST /api/channels/5678/mark-read

<- {}

Get messages in channel [GET /api/channels/:id/messages]

Returns { messages }, where messages is an array of the most recent messages sent to this channel. If limit is given, it’ll only fetch that many messages.

If before is specified, it’ll only return messages sent before that one; and it’ll only return messages sent after after.

GET /api/channels/5678/messages

<- {
<-   "messages": [
<-     {
<-       "id": "1234",
<-       "channelID": "5678",
<-       // ...
<-     },
<-     {
<-       "id": "1235",
<-       "channelID": "5678",
<-       // ...
<-     }
<-   ]
<- }
GET /api/channels/5678/messages?after=1234

<- {
<-   "messages": [
<-     {
<-       "id": "1235",
<-       "channelID": "5678",
<-       // ...
<-     }
<-   ]
<- }

Retrieve all pinned messages [GET /api/channels/:id/pins]

Returns { pins }, where pins is an array of messages that have been pinned to this channel.

GET /api/channels/5678/pins

<- {
<-   "pins": [
<-     {
<-       "id": "1235",
<-       "channelID": "5678",
<-       // ...
<-     }
<-   ]
<- }

Pin a message [POST /api/channels/:id/pins]

Returns {} if successful. Emits channel/pins/add.

POST /api/channels/5678/pins

-> {
->   "messageID": "1234"
-> }

<- {}

Unpin a message [DELETE /api/channels/:channelID/pins/:messageID]

Returns {} if successful. Emits channel/pins/remove.

DELETE /api/channels/5678/pins/1234

<- {}

Users

Model:

{
  "id": ID,
  "username": Name,

  "avatarURL": string,
  "permissionLevel": "admin" | "member",
  "flair": string | null,

  "online": boolean,

  "mentions": [ Message ] // List of messages that mention this user

  "authorized": boolean, // Only present if useAuthorization is true
  "email": string | null // Only provided if the requested user is the same as the sessionID provides
}

Related events:

Fetch users [GET /api/users]

Returns { users }, where users is an array of users. If an admin session is given, also returns unauthorizedUsers, an array of users who have not yet been authorized.

GET /api/users

<- {
<-   "users": [
<-     {
<-       "id": "1234",
<-       "username": "test-user",
<-       // ...
<-     }
<-   ]
<- }
GET /api/users?sessionID=adminsid123

<- {
<-   "users": [
<-     {
<-       "id": "1234",
<-       "username": "test-user",
<-       // ...
<-     }
<-   ],
<-   "unauthorizedUsers": [
<-     {
<-       "id": "5678",
<-       "username": "pls-let-me-join-pls-pls",
<-       // ...
<-     }
<-   ]
<- }

Register (create new user) [POST /api/users]

Responds with { user } if successful, where user is the new user object. If the server does not require authorization, user/new is emitted. Note the given password is passed as a plain string and is stored in the database as a bcrypt-hashed and salted string (and not in any plaintext form). Log in with POST /api/sessions.

POST /api/users

-> {
->   "username": "joe",
->   "password": "secret"
-> }

<- {
<-   "user": {
<-     "id": "8769",
<-     "username": "joe",
<-     // ...
<-   }
<- }

Retrieve a user by ID [GET /api/users/:id]

Returns { user }.

GET /api/users/1

<- {
<-   "user": {
<-     "id": "1",
<-     "username": "admin",
<-     // ...
<-   }
<- }

Update user details [PATCH /api/users/:id]

The following parameters are available to both admin sessions and sessions representing the user being updated:

You can provide an admin session in order to update the following, also:

Returns {} and applies changes, assuming a valid session for this user (or an admin) is provided and no errors occur. Also emits user/update.

PATCH /api/users/1

(with session representing user id 1)

-> {
->   "password": {
->     "old": "abcdef",
->     "new": "secure"
->   }
-> }

<- {}
PATCH /api/users/12

(with session representing an admin)

-> {
->   "permissionLevel": "admin",
->   "authorized": true,
->   "flair": null
-> }

<- {}

('flair: null' removes the user's flair.)

Check if a username is available [GET /api/username-available/:username]

On success, returns { available }, where available is a boolean for if the username is available or not. May return the error INVALID_NAME.

GET /api/username-available/patrick

<- {
<-   "available": false
<- }

Websocket Events

These are the events which are used to send (and receive) data specific to individual connections to the server, and for “live” updates (e.g. rather than having the client poll the server for new messages every 5 seconds, the server emits a message to the client’s web socket whenever a new message appears).

This project uses a WebSocket system which is similar to socket.io (though more simple). Messages sent to and from clients are JSON strings following the format { evt, data }, where evt is a name representing the meaning of the event, and data is an optional property specifying any additional data related to the event.

pingdata

Sent periodically (typically every 10 seconds) by the server, as well as immediately upon the client socket connecting. Clients should respond with a pongdata event, as described below.

pongdata

Should be sent from clients in response to pingdata. Notifies the server of any information related to the particular socket. Passed data should include:

message/new

Sent to all clients whenever a message is sent to any channel in the server. Passed data is in the format { message }, where message is a message representing the new message.

message/edit

Sent to all clients when any message is edited. Passed data is in the format { message }, where message is a message representing the new message.

channel/new

Sent to all clients when a channel is created. Passed data is in the format { channel }, where channel is a channel representing the new channel.

channel/update

Sent to all clients when a channel is updated (renamed, marked as read, etc). Passed data is in the format { channel }, including channel.unreadMessageCount if the socket is actively ponging sessionIDs.

channel/pins/add

Sent to all clients when a message is pinned to a channel. Passed data is in the format { message }, where message is the message that was pinned.

channel/pins/remove

Sent to all clients when a message is unpinned from a channel. Passed data is in the format { messageID }, where messageID is the ID of the message that was unpinned.

channel/delete

Sent to all clients when a channel is deleted. Passed data is in the format { channelID }.

user/new

Sent to all clients when a user is created, or instead when they are authorized, if the server requires authorization. Passed data is in the format { user }.

user/gone

Sent to all clients when a user is deleted, or when a user is deauthorized, if the server requires authorization. Passed data is in the format { userID }.

user/online

Sent to all clients when a user becomes online. This is whenever a socket tells the server that its session ID is that of a user who was not already online before. Passed data is in the format { userID }.

user/offline

Sent to all clients when a user becomes offline. This is whenever the last socket of a user who is online terminates. Passed data is in the format { userID }.

user/update

Sent to all clients when a user is mutated using PATCH /api/users/:userID. Passed data is in the format { user }.

user/mentions/add

When a user is mentioned, this is sent to all sockets authenticated as them. Passed data is in the format { message }, where message is the new / just edited mesage that mentioned the user.

user/mentions/remove

When a message is deleted or edited to remove the mention of a user, all sockets authenticated as the unmentioned user are sent this event. Passed data is in the format { messageID }, where messageID is the ID of the message that just stopped mentioning the user.

emote/new

Sent to all clients when an emote is added. Passed data is in the format { emote }, where emote is the new emote.

emote/delete

Sent to all clients when an emote is added. Passed data is in the format { shortcode }, where shortcode is the deleted emote’s shortcode.