From a15e3d580124f561864c6a39f1e035eb1b3aab13 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Mon, 30 Jun 2025 22:00:57 -0400 Subject: Rename "channel" to "conversation" within the server. I've split this from the schema and API changes because, frankly, it's huge. Annoyingly so. There are no semantic changes in this, it's all symbol changes, but there are a _lot_ of them because the term "channel" leaks all over everything in a service whose primary role is managing messages sent to channels (now, conversations). I found a buggy test while working on this! It's not fixed in this commit, because it felt mean to hide a real change in the middle of this much chaff. --- src/test/fixtures/event/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/test/fixtures/event/mod.rs') diff --git a/src/test/fixtures/event/mod.rs b/src/test/fixtures/event/mod.rs index 691cdeb..69c79d8 100644 --- a/src/test/fixtures/event/mod.rs +++ b/src/test/fixtures/event/mod.rs @@ -2,7 +2,7 @@ use crate::event::Event; pub mod stream; -pub fn channel(event: Event) -> Option { +pub fn conversation(event: Event) -> Option { match event { Event::Channel(channel) => Some(channel), _ => None, @@ -23,8 +23,8 @@ pub fn user(event: Event) -> Option { } } -pub mod channel { - use crate::channel::{Event, event}; +pub mod conversation { + use crate::conversation::{Event, event}; pub fn created(event: Event) -> Option { match event { -- cgit v1.2.3 From 8d412732dc094ead3c5cf86c005d187f9624fc65 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Tue, 1 Jul 2025 14:24:36 -0400 Subject: Replace `channel` with `conversation` throughout the API. This is a **breaking change** for essentially all clients. Thankfully, there's presently just the one, so we don't need to go to much effort to accommoate that; the client is modified in this commit to adapt, users can reload their client, and life will go on. --- docs/api/SUMMARY.md | 2 +- docs/api/authentication.md | 2 +- docs/api/boot.md | 10 +- docs/api/channels-messages.md | 235 ----------------------------- docs/api/conversations-messages.md | 231 ++++++++++++++++++++++++++++ docs/api/events.md | 97 ++++++------ docs/developer/server/code-organization.md | 2 +- docs/developer/server/testing.md | 2 +- src/event/mod.rs | 6 +- src/message/repo.rs | 12 +- src/message/snapshot.rs | 2 +- src/routes.rs | 6 +- src/test/fixtures/event/mod.rs | 2 +- ui/lib/apiServer.js | 4 +- ui/lib/session.svelte.js | 10 +- ui/lib/state/remote/messages.svelte.js | 12 +- ui/lib/state/remote/state.svelte.js | 18 +-- ui/routes/(app)/ch/[channel]/+page.svelte | 4 +- 18 files changed, 321 insertions(+), 336 deletions(-) delete mode 100644 docs/api/channels-messages.md create mode 100644 docs/api/conversations-messages.md (limited to 'src/test/fixtures/event/mod.rs') diff --git a/docs/api/SUMMARY.md b/docs/api/SUMMARY.md index 99b6352..f51fbc7 100644 --- a/docs/api/SUMMARY.md +++ b/docs/api/SUMMARY.md @@ -6,4 +6,4 @@ - [Client initialization](boot.md) - [Events](events.md) - [Invitations](invitations.md) -- [Channels and messages](channels-messages.md) +- [Conversations and messages](conversations-messages.md) diff --git a/docs/api/authentication.md b/docs/api/authentication.md index 2e9b58f..fbd5959 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -17,7 +17,7 @@ To create users, see [initial setup](./initial-setup.md) and [invitations](./inv ## Names - + The service handles user names using two separate forms. diff --git a/docs/api/boot.md b/docs/api/boot.md index f6e6dc2..d7e9144 100644 --- a/docs/api/boot.md +++ b/docs/api/boot.md @@ -52,7 +52,7 @@ This endpoint will respond with a status of "name": "example username" }, { - "type": "channel", + "type": "conversation", "event": "created", "at": "2025-04-14T23:58:11.421901Z", "id": "C1234abcd", @@ -62,7 +62,7 @@ This endpoint will respond with a status of "type": "message", "event": "sent", "at": "2024-09-27T23:19:10.208147Z", - "channel": "C1234abcd", + "conversation": "C1234abcd", "sender": "U1234abcd", "id": "M1312acab", "body": "beep" @@ -71,13 +71,13 @@ This endpoint will respond with a status of "type": "message", "event": "sent", "at": "2025-06-19T15:14:40.431627Z", - "channel": "Ccfdryfdb4krpy77", + "conversation": "Ccfdryfdb4krpy77", "sender": "U888j6fyc8ccrnkf", "id": "Mc6jk823wjc82734", "body": "test" }, { - "type": "channel", + "type": "conversation", "event": "created", "at": "2025-06-19T15:14:44.764263Z", "id": "C2d9y6wckph3n36x", @@ -87,7 +87,7 @@ This endpoint will respond with a status of "type": "message", "event": "sent", "at": "2025-06-19T15:29:47.376455Z", - "channel": "Ccfdryfdb4krpy77", + "conversation": "Ccfdryfdb4krpy77", "sender": "U888j6fyc8ccrnkf", "id": "M3twnj7rfk2ph744", "body": "test" diff --git a/docs/api/channels-messages.md b/docs/api/channels-messages.md deleted file mode 100644 index e762600..0000000 --- a/docs/api/channels-messages.md +++ /dev/null @@ -1,235 +0,0 @@ -# Channels and messages - -```mermaid ---- -Channel lifecycle ---- -stateDiagram-v2 - [*] --> Active : POST /api/channels - Active --> Deleted : DELETE /api/channels/C1234 - Active --> Deleted : Expiry - Deleted --> [*] : Purge -``` - -```mermaid ---- -Message lifecycle ---- -stateDiagram-v2 - [*] --> Sent : POST /api/channels/C1234 - Sent --> Deleted : DELETE /api/messages/Mabcd - Sent --> Deleted : Expiry - Deleted --> [*] : Purge -``` - -Messages allow logins to communicate with one another. Channels are the conversations to which those messages are sent. - -Every channel has a unique name, chosen when the channel is created. - -## Names - - - -The service handles channel names using two separate forms. - -The first form is as given in the request used to create the channel. This form of the channel name is used throughout the API, and the service will preserve the name as entered (other than applying normalization), so that users' preferences around capitalization and accent marks are preserved. - -The second form is a "canonical" form, used internally by the service to control uniqueness and match names to channels. The canonical form is both case-folded and normalized. - -The canonical form is not available to API clients, but its use has practical consequences. Names that differ only by case or only by code point sequence are treated as the same name. If the name is in use, changing the capitalization or changing the sequence of combining marks will not allow the creation of a second "identical" channel. - -## Expiry and purging - -Both channels and messages expire after a time. Messages expire 90 days after being sent. Channels expire 90 days after the last message sent to them, or after creation if no messages are sent in that time. - -Deleted channels and messages, including those that have expired, are temporarily retained by the service, to allow clients that are not connected to receive the corresponding deletion [events](./events.md). To limit storage growth, deleted channels and messages are purged from the service seven days after they were deleted. - -## `POST /api/channels` - -Creates a channel. - -### Request - -```json -{ - "name": "a unique channel name" -} -``` - -The request must have the following fields: - -| Field | Type | Description | -| :----- | :----- | :------------------ | -| `name` | string | The channel's name. | - -The proposed `name` must be valid. The precise definition of valid is still up in the air, but, at minimum: - -- It must be non-empty. -- It must not be "too long." (Currently, 64 characters is too long.) -- It must begin with a printing character. -- It must end with a printing character. -- It must not contain runs of multiple whitespace characters. - -### Success - -This endpoint will respond with a status of -`202 Accepted` when successful. The body of the response will be a JSON object describing the new channel: - -```json -{ - "id": "C9876cyyz", - "name": "a unique channel name" -} -``` - -The response will have the following fields: - -| Field | Type | Description | -| :----- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------- | -| `id` | string | A unique identifier for the channel. This can be used to associate the channel with events, or to make API calls targeting the channel. | -| `name` | string | The channel's name. | - -The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. - -When completed, the service will emit a [channel created](events.md#channel-created) event with the channel's ID. - -### Name not valid - -This endpoint will respond with a status of `400 Bad Request` if the proposed `name` is not valid. - -### Channel name in use - -This endpoint will respond with a status of `409 Conflict` if a channel with the requested name already exists. - -## `POST /api/channels/:id` - -Sends a message to a channel. - -This endpoint requires the following path parameter: - -| Parameter | Type | Description | -| :-------- | :----- | :------------ | -| `id` | string | A channel ID. | - -### Request - -```json -{ - "body": "my amazing thoughts, by bob" -} -``` - -The request must have the following fields: - -| Field | Type | Description | -| :----- | :----- | :------------------------------------- | -| `body` | string | The message to deliver to the channel. | - -### Success - -This endpoint will respond with a status of -`202 Accepted` when successful. The body of the response will be a JSON object describing the newly-sent message: - -```json -{ - "at": "2024-10-19T04:37:09.467325Z", - "channel": "Cfqdn1234", - "sender": "Uabcd1234", - "id": "Mgh98yp75", - "body": "my amazing thoughts, by bob" -} -``` - -The response will have the following fields: - -| Field | Type | Description | -| :-------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------------------- | -| `at` | timestamp | The moment the message was sent. | -| `channel` | string | The ID of the channel the message was sent to. | -| `sender` | string | The ID of the user that sent the message. | -| `id` | string | A unique identifier for the message. This can be used to associate the message with events, or to make API calls targeting the message. | -| `body` | string | The message's body. | - -The returned message body may not be identical to the body as sent, as the body will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned body will include this normalization; the service will use the normalized body elsewhere, and does not store the originally submitted body. - -When completed, the service will emit a [message sent](events.md#message-sent) event with the message's ID. - -### Invalid channel ID - -This endpoint will respond with a status of `404 Not Found` if the channel ID is not valid. - -## `DELETE /api/channels/:id` - -Deletes a channel. - -Deleting a channel prevents it from receiving any further messages. The channel must be empty; to delete a channel with messages in it, delete the messages first (or wait for them to expire). - -This endpoint requires the following path parameter: - -| Parameter | Type | Description | -| :-------- | :----- | :------------ | -| `id` | string | A channel ID. | - -### Success - -This endpoint will respond with a status of -`202 Accepted` when successful. The body of the response will be a JSON object describing the deleted channel: - -```json -{ - "id": "Cfqdn1234" -} -``` - -The response will have the following fields: - -| Field | Type | Description | -| :---- | :----- | :---------------- | -| `id` | string | The channel's ID. | - -When completed, the service will emit a [message deleted](events.md#message-deleted) event for each message in the channel, followed by a [channel deleted](events.md#channel-deleted) event with the channel's ID. - -### Channel not empty - -This endpoint will respond with a status of `409 Conflict` if the channel contains messages. - -### Invalid channel ID - -This endpoint will respond with a status of `404 Not Found` if the channel ID is not valid. - -## `DELETE /api/messages/:id` - -Deletes a message. - -This endpoint requires the following path parameter: - -| Parameter | Type | Description | -| :-------- | :----- | :------------ | -| `id` | string | A message ID. | - -### Success - -This endpoint will respond with a status of -`202 Accepted` when successful. The body of the response will be a JSON object describing the deleted message: - -```json -{ - "id": "Mgh98yp75" -} -``` - -The response will have the following fields: - -| Field | Type | Description | -| :---- | :----- | :---------------- | -| `id` | string | The message's ID. | - -When completed, the service will emit a [message deleted](events.md#message-deleted) event with the message's ID. - -### Invalid message ID - -This endpoint will respond with a status of `404 Not Found` if the message ID is not valid. - -### Not the sender - -This endpoint will respond with a status of `403 Forbidden` if the message was sent by a different login. diff --git a/docs/api/conversations-messages.md b/docs/api/conversations-messages.md new file mode 100644 index 0000000..c7995f7 --- /dev/null +++ b/docs/api/conversations-messages.md @@ -0,0 +1,231 @@ +# Conversations and messages + +```mermaid +--- +Conversation lifecycle +--- +stateDiagram-v2 + [*] --> Active : POST /api/conversations + Active --> Deleted : DELETE /api/conversations/C1234 + Active --> Deleted : Expiry + Deleted --> [*] : Purge +``` + +```mermaid +--- +Message lifecycle +--- +stateDiagram-v2 + [*] --> Sent : POST /api/conversations/C1234 + Sent --> Deleted : DELETE /api/messages/Mabcd + Sent --> Deleted : Expiry + Deleted --> [*] : Purge +``` + +Messages allow logins to communicate with one another. Conversations are the containers to which those messages are sent. + +Every conversation has a unique name, chosen when the conversation is created. + +## Names + + + +The service handles conversation names using two separate forms. + +The first form is as given in the request used to create the conversation. This form of the conversation name is used throughout the API, and the service will preserve the name as entered (other than applying normalization), so that users' preferences around capitalization and accent marks are preserved. + +The second form is a "canonical" form, used internally by the service to control uniqueness and match names to conversations. The canonical form is both case-folded and normalized. + +The canonical form is not available to API clients, but its use has practical consequences. Names that differ only by case or only by code point sequence are treated as the same name. If the name is in use, changing the capitalization or changing the sequence of combining marks will not allow the creation of a second "identical" conversation. + +## Expiry and purging + +Both conversations and messages expire after a time. Messages sent to a conversation will keep the conversation from expiring until the messages also expire. + +Deleted conversations and messages, including those that have expired, are temporarily retained by the service, to allow clients that are not connected to receive the corresponding deletion [events](./events.md). To limit storage growth, deleted conversations and messages are purged from the service seven days after they were deleted. + +## `POST /api/conversations` + +Creates a conversation. + +### Request + +```json +{ + "name": "a unique conversation name" +} +``` + +The request must have the following fields: + +| Field | Type | Description | +| :----- | :----- | :----------------------- | +| `name` | string | The conversation's name. | + +The proposed `name` must be valid. The precise definition of valid is still up in the air, but, at minimum: + +- It must be non-empty. +- It must not be "too long." (Currently, 64 characters is too long.) +- It must begin with a printing character. +- It must end with a printing character. +- It must not contain runs of multiple whitespace characters. + +### Success + +This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON object describing the new conversation: + +```json +{ + "id": "C9876cyyz", + "name": "a unique conversation name" +} +``` + +The response will have the following fields: + +| Field | Type | Description | +| :----- | :----- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | string | A unique identifier for the conversation. This can be used to associate the conversation with events, or to make API calls targeting the conversation. | +| `name` | string | The conversation's normalized name. | + +The returned name may not be identical to the name requested, as the name will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned name will include this normalization; the service will use the normalized name elsewhere, and does not store the originally requested name. + +When completed, the service will emit a [conversation created](events.md#conversation-created) event with the conversation's ID. + +### Name not valid + +This endpoint will respond with a status of `400 Bad Request` if the proposed `name` is not valid. + +### Conversation name in use + +This endpoint will respond with a status of `409 Conflict` if a conversation with the requested name already exists. + +## `POST /api/conversations/:id` + +Sends a message to a conversation. + +This endpoint requires the following path parameter: + +| Parameter | Type | Description | +| :-------- | :----- | :----------------- | +| `id` | string | A conversation ID. | + +### Request + +```json +{ + "body": "my amazing thoughts, by bob" +} +``` + +The request must have the following fields: + +| Field | Type | Description | +| :----- | :----- | :------------------------------------------ | +| `body` | string | The message to deliver to the conversation. | + +### Success + +This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON object describing the newly-sent message: + +```json +{ + "at": "2024-10-19T04:37:09.467325Z", + "conversation": "Cfqdn1234", + "sender": "Uabcd1234", + "id": "Mgh98yp75", + "body": "my amazing thoughts, by bob" +} +``` + +The response will have the following fields: + +| Field | Type | Description | +| :------------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------------------- | +| `at` | timestamp | The moment the message was sent. | +| `conversation` | string | The ID of the conversation the message was sent to. | +| `sender` | string | The ID of the user that sent the message. | +| `id` | string | A unique identifier for the message. This can be used to associate the message with events, or to make API calls targeting the message. | +| `body` | string | The message's normalized body. | + +The returned message body may not be identical to the body as sent, as the body will be converted to [normalization form C](http://www.unicode.org/reports/tr15/) automatically. The returned body will include this normalization; the service will use the normalized body elsewhere, and does not store the originally submitted body. + +When completed, the service will emit a [message sent](events.md#message-sent) event with the message's ID. + +### Invalid conversation ID + +This endpoint will respond with a status of `404 Not Found` if the conversation ID is not valid. + +## `DELETE /api/conversations/:id` + +Deletes a conversation. + +Deleting a conversation prevents it from receiving any further messages. The conversation must be empty; to delete a conversation with messages in it, delete the messages first (or wait for them to expire). + +This endpoint requires the following path parameter: + +| Parameter | Type | Description | +| :-------- | :----- | :----------------- | +| `id` | string | A conversation ID. | + +### Success + +This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON object describing the deleted conversation: + +```json +{ + "id": "Cfqdn1234" +} +``` + +The response will have the following fields: + +| Field | Type | Description | +| :---- | :----- | :--------------------- | +| `id` | string | The conversation's ID. | + +When completed, the service will emit a [message deleted](events.md#message-deleted) event for each message in the conversation, followed by a [conversation deleted](events.md#conversation-deleted) event with the conversation's ID. + +### Conversation not empty + +This endpoint will respond with a status of `409 Conflict` if the conversation contains messages. + +### Invalid conversation ID + +This endpoint will respond with a status of `404 Not Found` if the conversation ID is not valid. + +## `DELETE /api/messages/:id` + +Deletes a message. + +This endpoint requires the following path parameter: + +| Parameter | Type | Description | +| :-------- | :----- | :------------ | +| `id` | string | A message ID. | + +### Success + +This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON object describing the deleted message: + +```json +{ + "id": "Mgh98yp75" +} +``` + +The response will have the following fields: + +| Field | Type | Description | +| :---- | :----- | :---------------- | +| `id` | string | The message's ID. | + +When completed, the service will emit a [message deleted](events.md#message-deleted) event with the message's ID. + +### Invalid message ID + +This endpoint will respond with a status of `404 Not Found` if the message ID is not valid. + +### Not the sender + +This endpoint will respond with a status of `403 Forbidden` if the message was sent by a different login. diff --git a/docs/api/events.md b/docs/api/events.md index 570dffa..e692f82 100644 --- a/docs/api/events.md +++ b/docs/api/events.md @@ -28,13 +28,11 @@ sequenceDiagram end ``` -The core of the service is to facilitate conversations between users. Conversational activity is delivered to clients using -_events_. Each event notifies interested clients of activity sent to the service through its API. +The core of the service is to facilitate conversations between users. Conversational activity is delivered to clients using _events_. Each event notifies interested clients of activity sent to the service through its API. ## Asynchronous completion -A number of endpoints return -`202 Accepted` responses. The actions performed by those endpoints will be completed before events are delivered. To await the completion of an operation which returns this response, clients must monitor the event stream for the corresponding event. +A number of endpoints return `202 Accepted` responses. The actions performed by those endpoints will be completed before events are delivered. To await the completion of an operation which returns this response, clients must monitor the event stream for the corresponding event. ## `GET /api/events` @@ -46,14 +44,11 @@ This endpoint is designed for use with the [EventSource] DOM API, and supports s ### Query parameters -This endpoint requires a -`resume_point` (integer) query parameter. The event stream will collect events published after that point in time. The value must be obtained by calling the [ -`GET /api/boot`](./boot.md) method. +This endpoint requires a `resume_point` (integer) query parameter. The event stream will collect events published after that point in time. The value must be obtained by calling the [`GET /api/boot`](./boot.md) method. ### Request headers -This endpoint accepts an optional `last-event-id` (string) header. If present, the value must be the value of the -`id` field of the last message processed by the client. The returned event stream will begin with the following message. If absent, the returned event stream will begin from the start of the event collection. +This endpoint accepts an optional `last-event-id` (string) header. If present, the value must be the value of the `id` field of the last message processed by the client. The returned event stream will begin with the following message. If absent, the returned event stream will begin from the start of the event collection. This header is set automatically by `EventSource` when reconnecting to an event stream. @@ -69,7 +64,7 @@ data: { data: "type": "message", data: "event": "sent", data: "at": "2024-09-27T23:19:10.208147Z", -data: "channel": "C9876cyyz", +data: "conversation": "C9876cyyz", data: "sender": "U1234abcd", data: "id": "M1312acab", data: "body": "beep" @@ -78,9 +73,7 @@ data: } The service will keep the connection open, and will deliver events as they occur. -The service may terminate the connection at any time. Clients should reconnect and resume the stream, using the -`last-event-id` header to resume from the last message received. The -`id` of each event is an ephemeral ID, useful only for this purpose. +The service may terminate the connection at any time. Clients should reconnect and resume the stream, using the `last-event-id` header to resume from the last message received. The `id` of each event is an ephemeral ID, useful only for this purpose. Each event's `data` consists of a JSON object describing one event. Every event includes the following fields: @@ -133,66 +126,63 @@ These events have the `event` field set to `"created"`. They include the followi | `id` | string | A unique identifier for the newly-created user. This can be used to associate the user with other events, or to make API calls targeting the user. | | `name` | string | The user's name. | -## Channel events +## Conversation events -The following events describe changes to [channels](./channels-messages.md). +The following events describe changes to [conversations](conversations-messages.md). -These events have the `type` field set to `"channel"`. +These events have the `type` field set to `"conversation"`. -### Channel created +### Conversation created ```json { - "type": "channel", + "type": "conversation", "event": "created", "at": "2024-09-27T23:18:10.208147Z", "id": "C9876cyyz", - "name": "example channel 2" + "name": "example conversation 2" } ``` -Sent whenever a new channel is created. +Sent whenever a new conversation is created. These events have the `event` field set to `"created"`. They include the following additional fields: -| Field | Type | Description | -| :----------- | :------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `at` | timestamp | The moment the channel was created. | -| `id` | string | A unique identifier for the newly-created channel. This can be used to associate the channel with other events, or to make API calls targeting the channel. | -| `name` | string | The channel's name. | -| `deleted_at` | timestamp, optional | If set, the moment the channel was deleted. | +| Field | Type | Description | +| :----------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `at` | timestamp | The moment the conversation was created. | +| `id` | string | A unique identifier for the newly-created conversation. This can be used to associate the conversation with other events, or to make API calls targeting the conversation. | +| `name` | string | The conversation's name. | +| `deleted_at` | timestamp, optional | If set, the moment the conversation was deleted. | -When a channel is deleted or expires, the `"created"` event is replaced with a tombstone -`"created"` event, so that the original channel cannot be trivially recovered from the event stream. Tombstone events have a -`deleted_at` field, and a `name` of `""`. Tombstone events for channels use an empty string as the name, and not -`null` or with the `name` field removed entirely, to simplify client development. While clients -_should_ treat deleted channels specially, for example by rendering them as "channel deleted" markers, they don't have to be - they make sense if interpreted as channels with empty names, too. +When a conversation is deleted or expires, the `"created"` event is replaced with a tombstone +`"created"` event, so that the original conversation cannot be trivially recovered from the event stream. Tombstone events have a `deleted_at` field, and a `name` of `""`. Tombstone events for conversation use an empty string as the name, and not `null` or with the `name` field removed entirely, to simplify client development. While clients _should_ treat deleted conversation specially, for example by rendering them as "conversation deleted" markers, they don't have to be - they make sense if interpreted as conversation with empty names, too. -Once a deleted channel is [purged](./channels-messages.md#expiry-and-purging), these tombstone events are removed from the event stream. +Once a deleted conversation is [purged](conversations-messages.md#expiry-and-purging), these tombstone events are removed from the event stream. -### Channel deleted +### Conversation deleted ```json { - "type": "channel", + "type": "conversation", "event": "deleted", "at": "2024-09-28T03:40:25.384318Z", "id": "C9876cyyz" } ``` -Sent whenever a channel is deleted or expires. +Sent whenever a conversation is deleted or expires. These events have the `event` field set to `"deleted"`. They include the following additional fields: -| Field | Type | Description | -| :---- | :-------- | :---------------------------------- | -| `at` | timestamp | The moment the channel was deleted. | -| `id` | string | The deleted channel's ID. | +| Field | Type | Description | +| :---- | :-------- | :--------------------------------------- | +| `at` | timestamp | The moment the conversation was deleted. | +| `id` | string | The deleted conversation's ID. | ## Message events -The following events describe changes to [messages](./channels-messages.md). +The following events describe changes to [messages](conversations-messages.md). These events have the `type` field set to `"message"`. @@ -203,7 +193,7 @@ These events have the `type` field set to `"message"`. "type": "message", "event": "sent", "at": "2024-09-27T23:19:10.208147Z", - "channel": "C9876cyyz", + "conversation": "C9876cyyz", "sender": "U1234abcd", "id": "M1312acab", "body": "an effusive blob of prose, condensed down to a single string" @@ -214,22 +204,19 @@ Sent whenever a message is sent by a client. These events have the `event` field set to `"sent"`. They include the following additional fields: -| Field | Type | Description | -| :----------- | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| `at` | timestamp | The moment the message was sent. | -| `channel` | string | The ID of the channel the message was sent to. | -| `sender` | string | The ID of the user that sent the message. | -| `id` | string | A unique identifier for the message. This can be used to associate the message with other events, or to make API calls targeting the message. | -| `body` | string | The text of the message. | -| `deleted_at` | timestamp, optional | If set, the moment the message was deleted. | - -When a message is deleted or expires, the `"sent"` event is replaced with a tombstone -`"sent"` event, so that the original message cannot be trivially recovered from the event stream. Tombstone events have a -`deleted_at` field, and a `body` of `""`. Tombstone events for messages use an empty string as the `body`, and not -`null` or with the `body` field removed entirely, to simplify client development. While clients +| Field | Type | Description | +| :------------- | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| `at` | timestamp | The moment the message was sent. | +| `conversation` | string | The ID of the conversation the message was sent to. | +| `sender` | string | The ID of the user that sent the message. | +| `id` | string | A unique identifier for the message. This can be used to associate the message with other events, or to make API calls targeting the message. | +| `body` | string | The text of the message. | +| `deleted_at` | timestamp, optional | If set, the moment the message was deleted. | + +When a message is deleted or expires, the `"sent"` event is replaced with a tombstone `"sent"` event, so that the original message cannot be trivially recovered from the event stream. Tombstone events have a `deleted_at` field, and a `body` of `""`. Tombstone events for messages use an empty string as the `body`, and not `null` or with the `body` field removed entirely, to simplify client development. While clients _should_ treat deleted messages specially, for example by rendering them as "message deleted" markers, they don't have to be - they make sense if interpreted as messages with empty bodies, too. -Once a deleted message is [purged](./channels-messages.md#expiry-and-purging), these tombstone events are removed from the event stream. +Once a deleted message is [purged](conversations-messages.md#expiry-and-purging), these tombstone events are removed from the event stream. ### Message deleted diff --git a/docs/developer/server/code-organization.md b/docs/developer/server/code-organization.md index d17b604..3a691a2 100644 --- a/docs/developer/server/code-organization.md +++ b/docs/developer/server/code-organization.md @@ -6,7 +6,7 @@ Trust your gut, and reorganize to meet new needs. We've already revised this sch ## Topic modules -High-level concerns are grouped into topical modules. These include `crate::channel`, `crate::events`, `crate::login`, and others. Those modules generally contain their own app types, their own repo types, their own extractors, and any other supporting code they need. They may also provide an interface to other modules in the program. +High-level concerns are grouped into topical modules. These include `crate::conversation`, `crate::events`, `crate::login`, and others. Those modules generally contain their own app types, their own repo types, their own extractors, and any other supporting code they need. They may also provide an interface to other modules in the program. Most topic modules contain one or more of: diff --git a/docs/developer/server/testing.md b/docs/developer/server/testing.md index 8e87568..a3109cb 100644 --- a/docs/developer/server/testing.md +++ b/docs/developer/server/testing.md @@ -22,6 +22,6 @@ Prefer writing "flat" fixtures that do one thing, over compound fixtures that do Prefer role-specific names for test values: use, for example, `sender` for a login related to sending messages, rather than `login`. Fixture data is cheap, so make as many entities as make sense for the test. They'll vanish at the end of the test anyways. -Prefer testing a single endpoint at a time. Other interactions, which may be needed to set up the scenario or verify the results, should be done against the `app` abstraction directly. It's okay if this leads to redundant tests (see for example `src/channel/routes/test/on_send.rs` and `src/events/routes/test.rs`, which overlap heavily). +Prefer testing a single endpoint at a time. Other interactions, which may be needed to set up the scenario or verify the results, should be done against the `app` abstraction directly. It's okay if this leads to redundant tests (see for example `src/conversation/routes/test/on_send.rs` and `src/events/routes/test.rs`, which overlap heavily). Panicking in tests is fine. Panic messages should describe why the preconditions were expected, and can be terse. diff --git a/src/event/mod.rs b/src/event/mod.rs index 801bcb9..f41dc9c 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -20,7 +20,7 @@ pub use self::{ #[serde(tag = "type", rename_all = "snake_case")] pub enum Event { User(user::Event), - Channel(conversation::Event), + Conversation(conversation::Event), Message(message::Event), } @@ -38,7 +38,7 @@ impl Sequenced for Event { fn instant(&self) -> Instant { match self { Self::User(event) => event.instant(), - Self::Channel(event) => event.instant(), + Self::Conversation(event) => event.instant(), Self::Message(event) => event.instant(), } } @@ -52,7 +52,7 @@ impl From for Event { impl From for Event { fn from(event: conversation::Event) -> Self { - Self::Channel(event) + Self::Conversation(event) } } diff --git a/src/message/repo.rs b/src/message/repo.rs index 68f6e4a..9b65a67 100644 --- a/src/message/repo.rs +++ b/src/message/repo.rs @@ -54,7 +54,7 @@ impl Messages<'_> { .map(|row| History { message: Message { sent: Instant::new(row.sent_at, row.sent_sequence), - channel: row.conversation, + conversation: row.conversation, sender: row.sender, id: row.id, body: row.body.unwrap_or_default(), @@ -95,7 +95,7 @@ impl Messages<'_> { .map(|row| History { message: Message { sent: Instant::new(row.sent_at, row.sent_sequence), - channel: row.conversation, + conversation: row.conversation, sender: row.sender, id: row.id, body: row.body.unwrap_or_default(), @@ -132,7 +132,7 @@ impl Messages<'_> { .map(|row| History { message: Message { sent: Instant::new(row.sent_at, row.sent_sequence), - channel: row.conversation, + conversation: row.conversation, sender: row.sender, id: row.id, body: row.body.unwrap_or_default(), @@ -168,7 +168,7 @@ impl Messages<'_> { .map(|row| History { message: Message { sent: Instant::new(row.sent_at, row.sent_sequence), - channel: row.conversation, + conversation: row.conversation, sender: row.sender, id: row.id, body: row.body.unwrap_or_default(), @@ -274,7 +274,7 @@ impl Messages<'_> { message: Message { sent: Instant::new(row.sent_at, row.sent_sequence), id: row.id, - channel: row.conversation, + conversation: row.conversation, sender: row.sender, body: row.body.unwrap_or_default(), deleted_at: row.deleted_at, @@ -309,7 +309,7 @@ impl Messages<'_> { .map(|row| History { message: Message { sent: Instant::new(row.sent_at, row.sent_sequence), - channel: row.conversation, + conversation: row.conversation, sender: row.sender, id: row.id, body: row.body.unwrap_or_default(), diff --git a/src/message/snapshot.rs b/src/message/snapshot.rs index 0e6e9ae..12d4daa 100644 --- a/src/message/snapshot.rs +++ b/src/message/snapshot.rs @@ -8,7 +8,7 @@ use crate::{clock::DateTime, conversation, event::Instant, user}; pub struct Message { #[serde(flatten)] pub sent: Instant, - pub channel: conversation::Id, + pub conversation: conversation::Id, pub sender: user::Id, pub id: Id, pub body: Body, diff --git a/src/routes.rs b/src/routes.rs index ca4c60c..e38f744 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -29,13 +29,13 @@ pub fn routes(app: &App) -> Router { .route("/api/auth/login", post(user::handlers::login)) .route("/api/auth/logout", post(user::handlers::logout)) .route("/api/boot", get(boot::handlers::boot)) - .route("/api/channels", post(conversation::handlers::create)) + .route("/api/conversations", post(conversation::handlers::create)) .route( - "/api/channels/{channel}", + "/api/conversations/{conversation}", post(conversation::handlers::send), ) .route( - "/api/channels/{channel}", + "/api/conversations/{conversation}", delete(conversation::handlers::delete), ) .route("/api/events", get(event::handlers::stream)) diff --git a/src/test/fixtures/event/mod.rs b/src/test/fixtures/event/mod.rs index 69c79d8..08b17e7 100644 --- a/src/test/fixtures/event/mod.rs +++ b/src/test/fixtures/event/mod.rs @@ -4,7 +4,7 @@ pub mod stream; pub fn conversation(event: Event) -> Option { match event { - Event::Channel(channel) => Some(channel), + Event::Conversation(conversation) => Some(conversation), _ => None, } } diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js index 397638c..1bca6f6 100644 --- a/ui/lib/apiServer.js +++ b/ui/lib/apiServer.js @@ -28,11 +28,11 @@ export async function changePassword(password, to) { } export async function createChannel(name) { - return await apiServer.post('/channels', { name }).catch(responseError); + return await apiServer.post('/conversations', { name }).catch(responseError); } export async function postToChannel(channelId, body) { - return await apiServer.post(`/channels/${channelId}`, { body }).catch(responseError); + return await apiServer.post(`/conversations/${channelId}`, { body }).catch(responseError); } export async function deleteMessage(messageId) { diff --git a/ui/lib/session.svelte.js b/ui/lib/session.svelte.js index 838401c..0c73e00 100644 --- a/ui/lib/session.svelte.js +++ b/ui/lib/session.svelte.js @@ -11,7 +11,7 @@ import { DateTime } from 'luxon'; class Channel { static fromRemote({ at, id, name }, messages, meta) { const sentAt = messages - .filter((message) => message.channel === id) + .filter((message) => message.conversation === id) .map((message) => message.at); const lastEventAt = DateTime.max(at, ...sentAt); const lastReadAt = meta.get(id)?.lastReadAt; @@ -29,21 +29,21 @@ class Channel { } class Message { - static fromRemote({ id, at, channel, sender, body, renderedBody }, users) { + static fromRemote({ id, at, conversation, sender, body, renderedBody }, users) { return new Message({ id, at, - channel, + conversation, sender: users.get(sender), body, renderedBody, }); } - constructor({ id, at, channel, sender, body, renderedBody }) { + constructor({ id, at, conversation, sender, body, renderedBody }) { this.id = id; this.at = at; - this.channel = channel; + this.conversation = conversation; this.sender = sender; this.body = body; this.renderedBody = renderedBody; diff --git a/ui/lib/state/remote/messages.svelte.js b/ui/lib/state/remote/messages.svelte.js index 1be001b..852f29e 100644 --- a/ui/lib/state/remote/messages.svelte.js +++ b/ui/lib/state/remote/messages.svelte.js @@ -2,21 +2,21 @@ import { DateTime } from 'luxon'; import { render } from '$lib/markdown.js'; class Message { - static boot({ id, at, channel, sender, body }) { + static boot({ id, at, conversation, sender, body }) { return new Message({ id, at: DateTime.fromISO(at), - channel, + conversation, sender, body, renderedBody: render(body), }); } - constructor({ id, at, channel, sender, body, renderedBody }) { + constructor({ id, at, conversation, sender, body, renderedBody }) { this.id = id; this.at = at; - this.channel = channel; + this.conversation = conversation; this.sender = sender; this.body = body; this.renderedBody = renderedBody; @@ -26,8 +26,8 @@ class Message { export class Messages { all = $state([]); - add({ id, at, channel, sender, body }) { - const message = Message.boot({ id, at, channel, sender, body }); + add({ id, at, conversation, sender, body }) { + const message = Message.boot({ id, at, conversation, sender, body }); this.all.push(message); } diff --git a/ui/lib/state/remote/state.svelte.js b/ui/lib/state/remote/state.svelte.js index fb46489..ffc88c6 100644 --- a/ui/lib/state/remote/state.svelte.js +++ b/ui/lib/state/remote/state.svelte.js @@ -30,8 +30,8 @@ export class State { // Heartbeats are actually completely ignored here. They're handled in `Session`, but not as a // special case; _any_ event is a heartbeat event. switch (event.type) { - case 'channel': - return this.onChannelEvent(event); + case 'conversation': + return this.onConversationEvent(event); case 'user': return this.onUserEvent(event); case 'message': @@ -39,21 +39,21 @@ export class State { } } - onChannelEvent(event) { + onConversationEvent(event) { switch (event.event) { case 'created': - return this.onChannelCreated(event); + return this.onConversationCreated(event); case 'deleted': - return this.onChannelDeleted(event); + return this.onConversationDeleted(event); } } - onChannelCreated(event) { + onConversationCreated(event) { const { id, name } = event; this.channels.add({ id, name }); } - onChannelDeleted(event) { + onConversationDeleted(event) { const { id } = event; this.channels.remove(id); } @@ -80,8 +80,8 @@ export class State { } onMessageSent(event) { - const { id, at, channel, sender, body } = event; - this.messages.add({ id, at, channel, sender, body }); + const { id, at, conversation, sender, body } = event; + this.messages.add({ id, at, conversation, sender, body }); } onMessageDeleted(event) { diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index 87918f7..ef000fc 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -12,7 +12,9 @@ const channelId = $derived(page.params.channel); const channel = $derived(session.channels.find((channel) => channel.id === channelId)); - const messages = $derived(session.messages.filter((message) => message.channel === channelId)); + const messages = $derived( + session.messages.filter((message) => message.conversation === channelId), + ); const unsent = $derived(outbox.messages.filter((message) => message.channel === channelId)); const deleted = $derived(outbox.deleted.map((message) => message.messageId)); const unsentSkeletons = $derived( -- cgit v1.2.3