Describe events, commands, replies in more detail

This commit is contained in:
Joscha 2023-12-21 22:03:36 +01:00
parent f05f6882f9
commit 91b89bd297

562
API.md
View file

@ -57,139 +57,575 @@ id, the server must omit it as well.
### EventId
A message id is a string matching `e[0-9a-f]{16}`. In other words, it's a 128
bit hexadecimal integer prefixed by `e`.
A message id is a string matching the regex `e[0-9A-F]{16}`. It encodes 64 bit
unsigned integer.
Sorting events by their id puts them in chronological order.
Event ids are unique across rooms. Sorting events by their id puts them in
chronological order.
### MessageId
A message id is a string matching `m[0-9a-f]{16}`. In other words, it's a 128
bit hexadecimal integer prefixed by `m`.
A message id is a string matching the regex `m[0-9A-F]{16}`. It encodes a 64 bit
unsigned integer.
Sorting messages by their id puts them in chronological order.
Message ids are unique across rooms. Sorting messages by their id puts them in
chronological order.
### SessionId
A session id is a string matching `s[0-9a-f]{16}`. In other words, it's a 128
bit hexadecimal integer prefixed by `s`.
A session id is a string matching the regex `s[0-9A-F]{16}`. It encodes a 64 bit
unsigned integer.
### UserId
A user id is a string matching `[a-z0-9-]+`. It is usually four characters long
for accounts and eight characters plus a dash for other sessions, though this is
not required. A user id uniquely identifies a user.
A user id is a string matching the regex `u[0-9A-F]{16}`. It encodes a 64 bit
unsigned integer.
### Account
```ts
type Account = {
email: string;
displayName: string;
pingName: string;
};
```
### User
A participant in a room.
```ts
type User = {
id: UserId;
displayName: string;
localDisplayName?: string;
pingName?: string;
account?: true;
bot?: true;
host?: bool;
admin?: bool;
};
```
### Message
```ts
type Message = {
id: MessageId;
author: User;
content: string;
pinged: User[];
};
```
### RoomEvent
```ts
type RoomEvent =
| {
type: "enter";
id: EventId;
user: User;
}
| {
type: "exit";
id: EventId;
user: User;
}
| {
type: "user";
id: EventId;
user: User;
}
| {
type: "send";
id: EventId;
message: Message;
}
| {
type: "edit";
id: EventId;
by: User;
message: Message;
}
| {
type: "delete";
id: EventId;
by: User;
message: Message;
};
```
## Auth phase commands
### `c-auth-cookie`
These commands must only be sent during the auth phase.
### `auth-anon`
Obtain a unique user id for this session.
After the client has received the reply, the connection is in the roam phase.
```ts
type Command = {};
type Reply = {
id: UserId;
account?: Account;
};
```
### `auth-cookie`
Authenticate via the cookies exchanged during the HTTP handshake portion of the
WebSocket connection. This requires the client to store and present the cookies
on subsequent connections.
on subsequent connection handshakes.
### `c-auth-session-id`
After the client has received the reply, the connection is in the roam phase.
```ts
type Command = {};
type Reply = {
id: UserId;
account?: Account;
};
```
### `auth-session-id`
Authenticate via session id. This requires the client to store the session id
and present it on subsequent connections via `auth-session-id`.
and present it on subsequent connections via `auth-session-id`. The client must
send the last known id, if any. The server may return a different id from the id
the client sent.
### `c-auth-anon`
After the client has received the reply, the connection is in the roam phase.
Don't authenticate and obtain a unique user id for this session.
```ts
type Command = {
sessionId?: SessionId;
};
type Reply = {
sessionId: SessionId;
id: UserId;
account?: Account;
};
```
## Roam phase events
### `e-goodbye`
These events may occur during the roam phase.
### `goodbye`
When the server decides to close the connection, it may send a goodbye event
explaining the decision beforehand, though this is not required.
Possible reasons:
```ts
type Event = {
reason: string;
};
```
- protocol error (e.g. using roam commands during the auth phase)
- spam (the client is sending too many commands)
- login (the client has logged into an account on another connection for the same session)
- logout (the client has logged out of an account on another connection for the same session)
Reasons may include, but are not limited to:
- `protocol`: The client did not follow the API specification. Example: Using
roam commands during the auth phase.
- `spam`: The client sent too many commands.
- `login`: The client has logged into an account on either this connection or
another connection for the same session.
- `logout`: The client has logged out of an account on either this connection or
another connection for the same session.
## Roam phase commands
### `c-enter`
### `login`
TODO Describe `login` event
### `logout`
TODO Describe `logout` event
### `display-name`
TODO Describe `display-name` event
## Roam phase room events
### `enter`
A user has entered the room.
```ts
type Event = {
id: EventId;
room: string;
user: User;
};
```
### `exit`
A user has exited the room.
```ts
type Event = {
id: EventId;
room: string;
user: User;
};
```
### `user`
A property of a user has changed.
```ts
type Event = {
id: EventId;
room: string;
user: User;
};
```
### `send`
A new message has been sent.
```ts
type Event = {
id: EventId;
room: string;
message: Message;
};
```
### `edit`
An existing message has been edited.
```ts
type Event = {
id: EventId;
room: string;
by: User;
message: Message;
};
```
### `delete`
An existing message has been deleted.
```ts
type Event = {
id: EventId;
room: string;
by: User;
messageId: MessageId;
};
```
## Roam phase room commands
### `enter`
Enter a room. This is required for a client to receive room events and send room
commands. A user that has entered a room in at least one client will be displayed
in the nick list.
commands. A user that has entered a room in at least one client will be present
in the user list.
As part of the reply to this command, the server will include the client's
current room nick.
TODO Room authentication, failure in reply
### `c-exit`
This command is idempotent. Entering an already entered room is allowed and has
no special effect.
Note that a client won't witness its own `enter` event.
```ts
type Command = {
room: string;
};
type Reply = {
present: User[];
};
```
### `exit`
Exit a room. After exiting a room, the server will no longer relay events for
that room, and the client can no longer issue commands for that room.
### `c-login`
This command is idempotent. Exiting an already exited room is allowed and has no
special effect.
## Roam phase room events
Note that a client won't witness its own `exit` event.
### `e-enter`
```ts
type Command = {
room: string;
};
A user has entered the room.
type Reply = {};
```
### `e-exit`
### `local-display-name`
A user has exited the room.
Change or unset the local display name.
### `e-nick`
The server may reject the command because the user is not in the specified room.
If so, it will return the result `not-present`.
A user has changed their nick.
The server may reject the command for an arbitrary reason. If so, it will return
the result `rejected` and provide a human-readable reason in its reply.
### `e-send`
This command is idempotent.
A new message has been sent.
Note that this will cause a `user` event if the previous local display name was
different, and may cause a `user` event even if the previous local display name
was identical.
### `e-edit`
```ts
type Command = {
room: string;
localDisplayName?: string;
};
An existing message has been edited.
type Reply =
| {
result: "success";
user: User;
}
| { result: "not-present" }
| {
result: "rejected";
reason: string;
};
```
### `e-delete`
An existing message has been deleted.
## Roam phase room commands
### `c-nick`
Change your own nick.
### `c-send`
### `send`
Send a new message.
### `c-edit`
The server may reject the command because the user is not in the specified room.
If so, it will return the result `not-present`.
The server may reject the command because the parent does not exist. If so, it
will return the result `nonexistent-parent`.
The server may reject the command for an arbitrary reason. If so, it will return
the result `rejected` and provide a human-readable reason in its reply.
This command is idempotent if a `token` value is provided.
Note that this will cause a `send` event.
```ts
type Command = {
room: string;
token?: string;
parent?: MessageId;
content: string;
};
type Reply =
| {
result: "success";
message: Message;
}
| { result: "not-present" }
| { result: "nonexistent-parent" }
| {
result: "rejected";
reason: string;
};
```
### `edit`
Edit a message.
### `c-delete`
The server may reject the command because the user is not in the specified room.
If so, it will return the result `not-present`.
The server may reject the command because the user has insufficient permissions
to edit the message. If so, it will return the result
`insufficient-permissions`.
The server may reject the command because the message does not exist. If so, it
will return the result `nonexistent`.
The server may reject the command for an arbitrary reason. If so, it will return
the result `rejected` and provide a human-readable reason in its reply.
This command is idempotent.
Note that this will cause an `edit` event.
```ts
type Command = {
room: string;
messageId: MessageId;
content: string;
};
type Reply =
| {
result: "success";
message: Message;
}
| { result: "not-present" }
| { result: "insufficient-permissions" }
| { result: "nonexistent" }
| {
result: "rejected";
reason: string;
};
```
### `delete`
Delete a message.
### `c-get-msg`
TODO Think through how deletion interacts with the event log.
The server may reject the command because the user is not in the specified room.
If so, it will return the result `not-present`.
The server may reject the command because the user has insufficient permissions
to delete the message. If so, it will return the result
`insufficient-permissions`.
This command is idempotent. Deleting a non-existent message does not result in
an error.
Note that this will cause a `delete` event.
```ts
type Command = {
room: string;
messageId: MessageId;
content: string;
};
type Reply = {
result: "success" | "not-present" | "insufficient-permissions";
};
```
### `get-users`
Request a list of users currently present in a room.
This may be useful for clients like bots that don't want to track who is present
or not.
The server may reject the command because the user is not in the specified room.
If so, it will return the result `not-present`.
This command is idempotent because it has no server-side effects.
```ts
type Command = {
room: string;
};
type Reply =
| {
result: "success";
users: User[];
}
| { result: "not-present" };
```
### `get-message`
Request a specific message.
TODO Consider whether this should include previous edits
This may be useful for clients like bots that don't want to store the entire
message history.
### `c-get-thread`
The server may reject the command because the user is not in the specified room.
If so, it will return the result `not-present`.
Request one or more threads, optionally starting before a specific message id.
The server may reject the command because the message does not exist. If so, it
will return the result `nonexistent`.
If no message id is specified, the latest threads are returned. If a message id
is specified, the first few threads older than that id are returned.
This command is idempotent because it has no server-side effects.
### `c-get-log`
```ts
type Command = {
room: string;
};
Request event log, optionally starting before a specific event id.
type Reply =
| {
result: "success";
message: Message;
}
| { result: "not-present" }
| { result: "nonexistent" };
```
If no event id is specified, the latest events are returned. If an event id is
specified, the first few events older than that id are returned.
### `get-threads`
Request one or more threads.
If no amount is specified, the server chooses an arbitrary amount. The server
may return fewer messages than the specified amount.
If a message id is specified, the youngest few threads before the id are
returned. Otherwise, the youngest few threads are returned. Returned threads are
always consecutive and ordered ascending by id (old to young).
The server may reject the command because the user is not in the specified room.
If so, it will return the result `not-present`.
```ts
type Command = {
room: string;
amount?: number;
before?: MessageId;
};
type Reply =
| {
result: "success";
messages: Message[];
}
| { result: "not-present" };
```
### `get-events`
Request events from the event log.
If no amount is specified, the server chooses an arbitrary amount. The server
may return fewer messages than the specified amount.
If an event id is specified, the youngest few events before the id are returned.
Otherwise, the youngest few events are returned. Returned events are always
consecutive and ordered ascending by id (old to young).
The server may reject the command because the user is not in the specified room.
If so, it will return the result `not-present`.
```ts
type Command = {
room: string;
amount?: number;
before?: MessageId;
};
type Reply =
| {
result: "success";
events: RoomEvent[];
}
| { result: "not-present" };
```