Version | Released |
---|---|
Not yet |
Decent is an open-source messaging platform for the modern internet. It is inspired by closed-sourced platforms such as Discord and Slack, and intends to supercede both in terms of features and morality.
A common use-case for Decent is private servers between friends, however it is flexible enough to allow larger, more varied servers to exist. It can even be utilised as a platform for multiplayer text-based video games!
Note that this document acts as a preface/specification around doc.md, which pre-documents all WebSocket events and endpoints that servers MUST implement in full.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Note that "server" refers to a running instance of an implementation of this specification, and that "client" refers to any software attempting to communicate with the instance.
A "parameter" refers to either data passed via an HTTP request body or through an HTTP request's query parameters (that is, data given in the URL itself).
The data types "undefined", "null", "string", "boolean", "object", and "array" in this document are to be understood as described in ECMA 404. Also note "int" and "float", which are to be interpreted as integer and floating-point number respectively.
A type postfixed by a question mark (?
) should be taken as 'maybe', that is,
it must be either the type defined, not present, or null. For example, valid
int?
s include 5
, 15
, and null
.
Types delimeted by a pipe character (|
) are unions ie. logical OR. For
example, valid boolean | string
s include true
, false
, and "dog"
.
The id
type may be any scalar type (string | int | float | boolean
) however
it is up to the server to conclude which are valid. IDs MUST be unique for the
resource type they are respresenting. Servers SHOULD use a consistent datatype
for IDs; whether that is a random string, an ever-increasing integer, or
something else is an implementation detail.
Decent servers MUST support HTTP/1.1 as a form of data transfer. HTTP/1.0 and HTTP/2.0 support is OPTIONAL. Please refer to RFC 2616 and related specifications.
All HTTP endpoints other than /
MUST respond with valid JSON as described in
ECMA 404.
All HTTP endpoints (other than /api/upload-image
) that expect request bodies
SHOULD NOT accept any content type other than valid JSON. If an invalid body
is provided, the server SHOULD respond with a FAILED error as defined in
section 2.3: Error responses.
If a given request's query parameters or request body contains duplicate keys the request SHOULD be terminated with a REPEATED_PARAMETERS error. Likewise, if a request is missing expected parameters the request SHOULD be terminated with an INCOMPLETE_PARAMETERS error, and if a request's parameters are given but are of the wrong type, the server should respond with an INVALID_PARAMETER_TYPE error.
Valid HTTP requests for routes under /api/
MUST be responded to as detailed
in section 4.
GET requests for the route (/
) SHOULD serve sufficient information to connect
to the relevant server, for example a web-based client or instance details.
Other requests MUST be responded to with HTTP status code 404 Not Found. If the
request was for anything under /api/
, the server SHOULD respond with the
NOT_FOUND error code as described in section 2.3: Error responses.
If an error occurs during the processing of a request, the server SHOULD respond with the HTTP status code 500 Internal Server Error. See the following section (2.3: Error responses) for more information.
When the processing of an HTTP request for anything under /api/
fails, the
server SHOULD respond with a valid JSON body with the following form:
{
"error": {
"code": string,
...
}
}
The server SHOULD NOT respond with anything other than the error
object as
described here, however extra data under the error
object MAY be returned.
error.code
MUST be a string equal to one of the following:
- FAILED
- NO
- NOT_FOUND
- NOT_YOURS
- NOT_ALLOWED
- ALREADY_PERFORMED
- INCOMPLETE_PARAMETERS
- REPEATED_PARAMETERS
- INVALID_PARAMETER_TYPE
- INVALID_SESSION_ID
- INVALID_NAME
- NAME_ALREADY_TAKEN
- SHORT_PASSWORD
- INCORRECT_PASSWORD
The server SHOULD respond with a relevant HTTP status code with the error body, however it MAY simply use '200 OK' for all responses.
If the server responds with any error to a request, the request MUST have no action. For example, if any parameters are of the wrong type, the request MUST be a no-op.
Session IDs are unique identifier strings that are sent in HTTP requests from clients to identify the requester as a particular user. They SHOULD NOT be predictable in any way.
When processing an HTTP request at any endpoint under /api/
, the server MUST
search for a valid, non-expired session ID using the following three methods:
sessionID
query-string parametersessionID
in request bodyX-Session-ID
in request headers (MUST be case-insensitive)
If a single request provides a session ID multiple times, the server SHOULD respond with a REPEATED_PARAMETERS error as defined in section 2.3.
If a session ID is provided but is unknown, expired, or otherwise invalid, the server SHOULD immediately respond with an INVALID_SESSION_ID error. Otherwise, the session ID MUST be ignored.
Servers MUST ensure that valid session IDs actually identifies existing, non-deleted users.
Permissions are a way of limiting and granting certain abilities to groups of users, called 'roles'.
Permissions are stored within an object mapping permission key to
boolean | undefined
. A single permission in this object can have three states:
Value | Meaning |
---|---|
true |
Granted |
false |
Denied |
undefined |
Unset, follow cascade |
For example, a role that grants the sendMessages
permission but changes
nothing else would have the following permission object.
{
"sendMessages": true
}
See the permission table for a list of all permissions and their meanings.
Individual permissions MUST be computed according to the following algorithm.
- Let
k
be the permission key we are computing - Let
r
be an array of permission objects (from roles) applied to the user - Let
v
be the value of the keyk
of the value at index 0 ofr
; ie.r[0][k]
- If
v
istrue
, returntrue
(granted) - If
v
isfalse
, returnfalse
(denied explicitly) - If
v
isundefined
or the keyk
is not present in the object, continue to step 7 - Repeat from step 3 for the next value in
r
(index++
). If we reach the end ofr
without returning, returnfalse
(denied by default)
If a request requires a permission and the requesting user/guest does not have the required permission as per the above algorithm, the server MUST respond with a NOT_ALLOWED error (and, therefore, be a no-op).
The order of r
(role priority) MUST be sorted in the following way:
- Channel-specific permissions for roles of the user (First.)
- Channel-specific permissions for the internal
_user
role, if the user is a logged-in member of the server - Channel-specific permissions for the internal
_everyone
role - Server-wide permissions for roles of the user
- Server-wide permissions for the
_user
role (if applicable, as above) - Server-wide permissions for the
_everyone
role (Last.)
Permissions for roles applied to a user (both server-wide and channel-specific)
MUST be prioritized according to the role prioritization order (see
PATCH /api/roles/order
in section 4.3). Note that the order of any given
user's roles
property MUST NOT have any effect on the order roles are
applied when calculating their permissions.
This specification defines two 'internal roles.' That is, they are applied under-the-hood to users under different circumstances. They are:
role.id | role.permissions | Applied to |
---|---|---|
_everyone | All false |
All requests |
_user | { sendMessages: true } |
Any logged-in request |
Servers MUST apply based on whether the requester provided a valid session ID or not (see section 2.4):
Servers MUST NOT allow any of these constant roles to be deleted, or updated at a global level. However, either role's permissions MUST be editable on a channel-specific basis.
Note: Servers MAY give a particular group of users some kind of automatically generated 'administrator' role to allow changes to be made by the owner of the server without manually editing the database. These roles are not classed as internal.
Decent servers MUST support the WebSocket protocol as specified in RFC 6455 for client-server communication.
The server MUST support WebSocket handshakes at the root (/
) HTTP endpoint,
but MAY also support connection upgrades at any route.
The server MUST support as many open WebSocket connections at once as possible.
Note that, in this document, "emit" refers to the act of sending data via a WebSocket to a client. A client response is the act of sending data via a WebSocket to the server.
All server-sent data down the WebSocket MUST be formatted in JSON with the following form:
{
"evt": string,
...
}
Any extra data MUST be provided within an object data
, for example:
{
"evt": "pingdata",
"data": {
"sessionID": "secret"
}
}
Valid event (evt
) strings to be sent from the server are defined later in this
document alongside related HTTP endpoints.
The server MUST NOT send invalid JSON (as described in ECMA 404) down the wire.
Invalid JSON or unknown events received from client sockets SHOULD be ignored.
The server MUST send {"evt": "pingdata"}
to all connected client sockets
periodically (preferably no longer than every 30 seconds). The server MAY send
this event immediately upon the client socket's connection. Clients SHOULD
respond with a "pongdata"
event, as described below.
The server MUST acknowledge the "pongdata"
event when sent from client
sockets. It MUST NOT be sent from the server at any time. This event follows the
following form:
{
"evt": "pingdata",
"data": { "sessionID": string? }
}
If the server receives a valid "pingdata"
event from a connected client
socket, it MUST associate the sender socket with the provided sessionID
. This
data will later be used to determine whether particular events should be sent
to the socket or not. Likewise, if the sessionID is not provided, null, or
otherwise invalid, the server MUST unassociate the sender socket from any user
or session.
Upon receiving a valid "pongdata"
event where data.sessionID
is not null or
undefined and is a known session ID (see section 2.4: Session IDs), the server
SHOULD mark the related user as 'online' (see section 3.3).
After a reasonable amount of time after a "pingdata"
event is emitted, the
server SHOULD check for any users that have not sent "pongdata"
events tied
to them. These users SHOULD all be marked as 'offline'.
Servers MAY use other criteria to determine user online/offline status.
A user that is marked as online should be logged in, however note that this state is an approximation.
See the previous section, 3.2: "pingdata" and "pongdata" events.
When a user is marked as online and was offline previously, the server SHOULD
emit the "user/online"
event to all connected client sockets, where
data.userID
is the ID of the user that has just been marked online. Servers
MUST emit this event using the following form:
{
"evt": "user/online",
"data": {
"userID": id
}
}
When a user is decidedly marked offline and was online previously, the server
SHOULD emit the "user/online"
event to all connected client sockets. It is
similar to the "user/online"
event:
{
"evt": "user/offline",
"data": {
"userID": id
}
}
For more details on the user-facing API of any of the following endpoints or events, see doc.md. If a server cannot handle or does not implement a particular endpoint for whatever reason, it MUST respond with an error (code NO) with the form specified in section 2.3.
Servers SHOULD implement all endpoints and events fully.
Type | Endpoint / event name |
---|---|
event | pingdata |
event | pongdata (from client) |
GET | /api/ |
GET | /api/settings |
PATCH | /api/settings |
event | server-settings/update |
POST | /api/upload-image |
Type | Endpoint / event name |
---|---|
event | user/new |
event | user/delete |
event | user/online |
event | user/offline |
event | user/update |
event | user/mentions/add |
event | user/mentions/remove |
GET | /api/users |
POST | /api/users |
GET | /api/users/:id |
PATCH | /api/users/:id |
DELETE | /api/users/:id |
GET | /api/users/:id/permissions |
GET | /api/users/:id/mentions |
GET | /api/users/:userID/channel-permissions/:channelID** |
GET | /api/username-available/:username |
Type | Endpoint / event name |
---|---|
event | role/new |
event | role/update |
event | role/delete |
GET | /api/roles |
POST | /api/roles |
GET | /api/roles/order |
PATCH | /api/roles/order |
GET | /api/roles/:id |
PATCH | /api/roles/:id |
DELETE | /api/roles/:id |
Type | Endpoint / event name |
---|---|
event | message/new |
event | message/edit |
event | message/delete |
POST | /api/messages |
GET | /api/messages/:id |
PATCH | /api/messages/:id |
DELETE | /api/messages/:id |
Type | Endpoint / event name |
---|---|
event | channel/new |
event | channel/update |
event | channel/delete |
event | channel/pins/add |
event | channel/pins/remove |
GET | /api/channels |
POST | /api/channels |
GET | /api/channels/:id |
PATCH | /api/channels/:id |
DELETE | /api/channels/:id |
POST | /api/channels/:id/mark-read |
GET | /api/channels/:id/messages |
PATCH | /api/channels/:id/role-permissions |
GET | /api/channels/:id/pins |
POST | /api/channels/:id/pins |
DELETE | /api/channels/:channelID/pins/:messageID |
Type | Endpoint / event name |
---|---|
event | emote/new |
event | emote/delete |
GET | /api/emotes |
POST | /api/emotes |
GET | /api/emotes/:shortcode |
DELETE | /api/emotes/:shortcode |
Type | Endpoint / event name |
---|---|
GET | /api/sessions |
POST | /api/sessions |
GET | /api/sessions/:id |
DELETE | /api/sessions/:id |