Communicating with the API
Misc
When a request is made to the API, the server searches for a session ID given in the request using:
sessionID
in POST body?sessionID
in query stringX-Session-ID
headerEndpoints 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 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.
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.
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.
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:
NOT_FOUND
- For when you try to request a something, but it isn’t found (e.g. requesting the user by the name foobar
when there is no such user).NOT_YOURS
- For when you attempt to do something impactful (e.g. editing/deleting) to a something you aren’t the owner/author of.MUST_BE_ADMIN
- For when you try to do something limited to admins, but you are not an admin.ALREADY_PERFORMED
- For when you try to do something, but you have already done that something (e.g. pinning a message you’ve already pinned).INCOMPLETE_PARAMETERS
- For when a property is missing from a request’s parameters. The missing property’s name is passed in error.missing
.INVALID_PARAMETER_TYPE
- For when a property is given in a request’s parameters, but is not the right type (e.g. passing a string instead of an array). The invalid property’s name is passed in error.invalidParameter
.
SHORT_PASSWORD
, are responded for issues that might be related to user input.INVALID_SESSION_ID
- For when a session ID is passed, but there is no session with that ID. (This is for general usage where being logged in is required. For /sessions/:sessionID
, NOT_FOUND
is returned if there is no session with the given ID.)UPLOAD_FAILED
- For when an upload fails.NAME_ALREADY_TAKEN
- For when you try to create a something, but your passed name is already taken by another something (e.g. registering a username which is already used by someone else).SHORT_PASSWORD
- For when you attempt to register but your password is too short.INCORRECT_PASSWORD
- For when you attempt to log in but you didn’t enter the right password. (Note that NOT_FOUND
is returned if you try to log in with an unused username.)INVALID_NAME
- For when you try to make something (a user or channel, etc) with an invalid name.All endpoints respond in JSON, and those which take POST bodies expect it to be formatted using JSON.
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"
<- }
Model:
{
"name": string,
"authorizationMessage": string
}
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."
<- }
<- }
name
(string; optional)authorizationMessage
(string; optional)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 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
}
Returns { properties }
, where properties
is an object representing server-specific properties.
GET /api/properties
<- {
<- "properties": {
<- "useSecure": false,
<- "useAuthorization": false
<- }
<- }
multipart/form-data
)
image
(gif/jpeg/png) - The image to upload. Max size: 10MBReturns { 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.
Model:
{
"shortcode": Name, // Without colons
"imageURL": string
}
Related events:
Returns { emotes }
, where emotes
is an array of emote objects.
GET /api/emotes
<- {
<- "emotes": []
<- }
imageURL
(string)shortcode
(Name) - Should not include colons (:
).Returns {}
if successful. Emits emote/new.
POST /api/emotes
-> {
-> "imageURL": "https://example.com/path/to/emote.png",
-> "shortcode": "package"
-> }
<- {}
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'/>
Returns {}
if successful. Emits emote/delete.
DELETE /api/emotes/package
<- {}
Model:
{
"id": string,
"dateCreated": number
}
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
<- }
<- ]
<- }
username
(string)password
(string)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"
<- }
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",
<- // ...
<- }
<- }
Responds with {}
upon success. Any further requests using the provided session ID will fail.
DELETE /api/sessions/12345678-ABCDEFGH
<- {}
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:
channelID
(ID) - The parent channel of the new messagetext
(string) - The content of the messageOn 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"
<- }
Returns { message }
where message
is a message object.
GET /api/messages/1234
<- {
<- "message": {
<- "id": "1234",
<- // ...
<- }
<- }
text
(string) - The new content of the messageEmits 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.
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.
Model:
{
"id": ID,
"name": string // Does not include a hash
}
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:
unreadMessageCount
) with sessionReturns { 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"
<- }
<- ]
<- }
name
(name) - The name of the channel.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.
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.
Returns {}
if successful, emitting channel/update.
PATCH /api/channels/5678
-> {
-> "name": "best-channel"
-> }
<- {}
Returns {}
if successful. Emits channel/delete.
DELETE /api/channels/5678
<- {}
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
<- {}
before
(ID; optional) - The ID of the message right after the range of messages you want.after
(ID; optional) - The ID of the message right before the range of messages you want.limit
(integer; optional, default 50
) - The maximum number of messages to fetch. Must be 1 <= limit <= 50
.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",
<- // ...
<- }
<- ]
<- }
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",
<- // ...
<- }
<- ]
<- }
messageID
(ID) - The message to pin to this channel.Returns {}
if successful. Emits channel/pins/add.
POST /api/channels/5678/pins
-> {
-> "messageID": "1234"
-> }
<- {}
Returns {}
if successful. Emits channel/pins/remove.
DELETE /api/channels/5678/pins/1234
<- {}
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:
unauthorizedUsers
) with admin sessionReturns { 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",
<- // ...
<- }
<- ]
<- }
username
(name) - Must be uniquepassword
(string) - Errors if shorter than 6 charactersResponds 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",
<- // ...
<- }
<- }
email
) with sessionReturns { user }
.
GET /api/users/1
<- {
<- "user": {
<- "id": "1",
<- "username": "admin",
<- // ...
<- }
<- }
The following parameters are available to both admin sessions and sessions representing the user being updated:
password
(object; optional):
new
(string) - Errors if shorter than 6 charactersold
(string) - Errors if it doesn’t match user’s existing passwordemail (string |
null; optional) - Not public, used to generate avatar URL |
flair (string |
null; optional) - Displayed beside username in chat, errors if longer than 50 characters |
You can provide an admin session in order to update the following, also:
permissionLevel
: (“admin” or “member”; optional)authorized
: (boolean; optional) - Errors (AUTHORIZATION_ERROR
) if the server does not require authorizationReturns {}
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.)
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
<- }
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.
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.
Should be sent from clients in response to pingdata
. Notifies the server of any information related to the particular socket. Passed data should include:
sessionID
, if the client is “logged in” or keeping track of a session ID. This is used for keeping track of which users are online.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.
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.
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.
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.
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.
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.
Sent to all clients when a channel is deleted. Passed data is in the format { channelID }
.
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 }
.
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 }
.
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 }
.
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 }
.
Sent to all clients when a user is mutated using PATCH /api/users/:userID. Passed data is in the format { user }
.
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.
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.
Sent to all clients when an emote is added. Passed data is in the format { emote }
, where emote
is the new emote.
Sent to all clients when an emote is added. Passed data is in the format { shortcode }
, where shortcode
is the deleted emote’s shortcode.