diff options
70 files changed, 387 insertions, 401 deletions
@@ -113,17 +113,6 @@ dependencies = [ ] [[package]] -name = "async-trait" -version = "0.1.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "atoi" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -140,13 +129,13 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.7.9" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ - "async-trait", "axum-core", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -174,11 +163,10 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" dependencies = [ - "async-trait", "bytes", "futures-util", "http", @@ -195,25 +183,25 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.6" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" dependencies = [ "axum", "axum-core", "bytes", "cookie", - "fastrand", + "form_urlencoded", "futures-util", "headers", "http", "http-body", "http-body-util", "mime", - "multer", "pin-project-lite", "serde", "serde_html_form", + "serde_path_to_error", "tower", "tower-layer", "tower-service", @@ -519,15 +507,6 @@ dependencies = [ ] [[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1218,9 +1197,9 @@ checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "md-5" @@ -1265,23 +1244,6 @@ dependencies = [ ] [[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - -[[package]] name = "num-bigint-dig" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1409,7 +1371,6 @@ name = "pilcrow" version = "0.1.0" dependencies = [ "argon2", - "async-trait", "axum", "axum-extra", "chrono", @@ -1429,7 +1390,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "thiserror 1.0.69", + "thiserror", "tokio", "tokio-stream", "unicode-casefold", @@ -1876,7 +1837,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.11", + "thiserror", "tokio", "tokio-stream", "tracing", @@ -1960,7 +1921,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.11", + "thiserror", "tracing", "whoami", ] @@ -1998,7 +1959,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.11", + "thiserror", "tracing", "whoami", ] @@ -2100,31 +2061,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.11", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1,8 +1,8 @@ [package] name = "pilcrow" version = "0.1.0" -edition = "2021" -rust-version = "1.82" +edition = "2024" +rust-version = "1.85" authors = [ "Owen Jacobson <owen@grimoire.ca>", "Kit La Touche <kit@transneptune.net>", @@ -23,9 +23,8 @@ assets = [ [dependencies] argon2 = "0.5.3" -async-trait = "0.1.86" -axum = { version = "0.7.9", features = ["form"] } -axum-extra = { version = "0.9.6", features = ["cookie", "query", "typed-header"] } +axum = { version = "0.8.1", features = ["form"] } +axum-extra = { version = "0.10.0", features = ["cookie", "query", "typed-header"] } chrono = { version = "0.4.39", features = ["serde"] } clap = { version = "4.5.30", features = ["derive", "env"] } futures = "0.3.31" @@ -45,7 +44,7 @@ serde_json = "1.0.138" # Pinned to maintain libsqlite3 version match between this and rusqlite. See # also: <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html> sqlx = { version = "=0.8.3", features = ["chrono", "runtime-tokio", "sqlite"] } -thiserror = "1.0.69" +thiserror = "2.0.11" tokio = { version = "1.43.0", features = ["rt", "macros", "rt-multi-thread"] } tokio-stream = { version = "0.1.17", features = ["sync"] } unicode-casefold = "0.2.0" diff --git a/docs/api/channels-messages.md b/docs/api/channels-messages.md index 2aa8ac5..a3c90be 100644 --- a/docs/api/channels-messages.md +++ b/docs/api/channels-messages.md @@ -26,25 +26,30 @@ Messages allow logins to communicate with one another. Channels are the conversa Every channel has a unique name, chosen when the channel is created. - ## Names <!-- This prose is duplicated in authentication.md. If you change it here, consider changing it there, too. --> 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 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 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. +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. +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` @@ -54,14 +59,14 @@ Creates a channel. ```json { - "name": "a unique channel name" + "name": "a unique channel name" } ``` The request must have the following fields: -| Field | Type | Description | -|:-------|:-------|:--| +| 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: @@ -74,23 +79,26 @@ The proposed `name` must be valid. The precise definition of valid is still up i ### 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: +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", + "id": "C9876cyyz", + "name": "a unique channel name" } ``` The response will have the following fields: -| Field | Type | Description | -|:-------|:-------|:--| +| 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. | +| `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. +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. @@ -102,56 +110,58 @@ This endpoint will respond with a status of `400 Bad Request` if the proposed `n 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 | -|:----------|:-------|:--| +| Parameter | Type | Description | +|:----------|:-------|:--------------| | `id` | string | A channel ID. | ### Request ```json { - "body": "my amazing thoughts, by bob" + "body": "my amazing thoughts, by bob" } ``` The request must have the following fields: -| Field | Type | Description | -|:-------|:-------|:--| +| 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: +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": "Labcd1234", - "id": "Mgh98yp75", - "body": "an elaborate example message" + "at": "2024-10-19T04:37:09.467325Z", + "channel": "Cfqdn1234", + "sender": "Labcd1234", + "id": "Mgh98yp75", + "body": "an elaborate example message" } ``` 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 login that sent the message. | +| 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 login 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. | +| `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. +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. @@ -159,36 +169,38 @@ When completed, the service will emit a [message sent](events.md#message-sent) e 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). +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 | -|:----------|:-------|:--| +| 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: +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" + "id": "Cfqdn1234" } ``` The response will have the following fields: -| Field | Type | Description | -|:----------|:----------|:--| -| `id` | string | The channel's ID. | +| 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. +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 @@ -198,32 +210,32 @@ This endpoint will respond with a status of `409 Conflict` if the channel contai 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 | -|:----------|:-------|:--| +| 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: +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" + "id": "Mgh98yp75" } ``` The response will have the following fields: -| Field | Type | Description | -|:----------|:----------|:--| -| `id` | string | The message's ID. | +| 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. diff --git a/docs/mission.md b/docs/mission.md new file mode 100644 index 0000000..ab86fd9 --- /dev/null +++ b/docs/mission.md @@ -0,0 +1,3 @@ +# Mission + +The one thing it has to do, pretty much no matter what, is deliver messages. If it can't do that, it should be down, so that nobody thinks it's delivering messages. diff --git a/src/boot/routes/mod.rs b/src/boot/routes/mod.rs index e4d5ac8..8fd99d3 100644 --- a/src/boot/routes/mod.rs +++ b/src/boot/routes/mod.rs @@ -1,4 +1,4 @@ -use axum::{routing::get, Router}; +use axum::{Router, routing::get}; use crate::app::App; diff --git a/src/broadcast.rs b/src/broadcast.rs index bedc263..2792a18 100644 --- a/src/broadcast.rs +++ b/src/broadcast.rs @@ -1,8 +1,8 @@ use std::sync::{Arc, Mutex}; -use futures::{future, stream::StreamExt as _, Stream}; -use tokio::sync::broadcast::{channel, Sender}; -use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}; +use futures::{Stream, future, stream::StreamExt as _}; +use tokio::sync::broadcast::{Sender, channel}; +use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; // Clones will share the same sender. #[derive(Clone)] @@ -47,7 +47,7 @@ where // panic: if ``channel`` has not been previously registered, and was not // part of the initial set of channels. - pub fn subscribe(&self) -> impl Stream<Item = M> + std::fmt::Debug { + pub fn subscribe(&self) -> impl Stream<Item = M> + std::fmt::Debug + use<M> { let rx = self.sender().subscribe(); BroadcastStream::from(rx).scan((), |(), r| { diff --git a/src/channel/app.rs b/src/channel/app.rs index 1e341e3..dc9e584 100644 --- a/src/channel/app.rs +++ b/src/channel/app.rs @@ -3,13 +3,14 @@ use itertools::Itertools; use sqlx::sqlite::SqlitePool; use super::{ + Channel, Id, repo::{LoadError, Provider as _}, - validate, Channel, Id, + validate, }; use crate::{ clock::DateTime, db::{Duplicate as _, NotFound as _}, - event::{repo::Provider as _, Broadcaster, Event, Sequence}, + event::{Broadcaster, Event, Sequence, repo::Provider as _}, message::{self, repo::Provider as _}, name::{self, Name}, }; diff --git a/src/channel/history.rs b/src/channel/history.rs index ef2120d..4af46ce 100644 --- a/src/channel/history.rs +++ b/src/channel/history.rs @@ -1,8 +1,8 @@ use itertools::Itertools as _; use super::{ - event::{Created, Deleted, Event}, Channel, Id, + event::{Created, Deleted, Event}, }; use crate::event::{Instant, Sequence}; @@ -42,7 +42,7 @@ impl History { // Event factories impl History { - pub fn events(&self) -> impl Iterator<Item = Event> { + pub fn events(&self) -> impl Iterator<Item = Event> + use<> { [self.created()] .into_iter() .merge_by(self.deleted(), Sequence::merge) diff --git a/src/channel/repo.rs b/src/channel/repo.rs index 6612151..91f245b 100644 --- a/src/channel/repo.rs +++ b/src/channel/repo.rs @@ -1,5 +1,5 @@ use futures::stream::{StreamExt as _, TryStreamExt as _}; -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; use crate::{ channel::{Channel, History, Id}, @@ -13,7 +13,7 @@ pub trait Provider { fn channels(&mut self) -> Channels; } -impl<'c> Provider for Transaction<'c, Sqlite> { +impl Provider for Transaction<'_, Sqlite> { fn channels(&mut self) -> Channels { Channels(self) } @@ -21,7 +21,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Channels<'t>(&'t mut SqliteConnection); -impl<'c> Channels<'c> { +impl Channels<'_> { pub async fn create(&mut self, name: &Name, created: &Instant) -> Result<History, sqlx::Error> { let id = Id::generate(); let name = name.clone(); diff --git a/src/channel/routes/channel/post.rs b/src/channel/routes/channel/post.rs index b51e691..3f14d64 100644 --- a/src/channel/routes/channel/post.rs +++ b/src/channel/routes/channel/post.rs @@ -8,7 +8,7 @@ use crate::{ app::App, clock::RequestedAt, error::{Internal, NotFound}, - message::{app::SendError, Body, Message}, + message::{Body, Message, app::SendError}, token::extract::Identity, }; diff --git a/src/channel/routes/mod.rs b/src/channel/routes/mod.rs index 696bd72..c917348 100644 --- a/src/channel/routes/mod.rs +++ b/src/channel/routes/mod.rs @@ -1,6 +1,6 @@ use axum::{ - routing::{delete, post}, Router, + routing::{delete, post}, }; use crate::app::App; @@ -14,6 +14,6 @@ mod test; pub fn router() -> Router<App> { Router::new() .route("/api/channels", post(post::handler)) - .route("/api/channels/:channel", post(channel::post::handler)) - .route("/api/channels/:channel", delete(channel::delete::handler)) + .route("/api/channels/{channel}", post(channel::post::handler)) + .route("/api/channels/{channel}", delete(channel::delete::handler)) } diff --git a/src/channel/routes/post.rs b/src/channel/routes/post.rs index 2cf1cc0..72eaad6 100644 --- a/src/channel/routes/post.rs +++ b/src/channel/routes/post.rs @@ -6,7 +6,7 @@ use axum::{ use crate::{ app::App, - channel::{app, Channel}, + channel::{Channel, app}, clock::RequestedAt, error::Internal, name::Name, diff --git a/src/channel/snapshot.rs b/src/channel/snapshot.rs index 129c0d6..046ac38 100644 --- a/src/channel/snapshot.rs +++ b/src/channel/snapshot.rs @@ -1,6 +1,6 @@ use super::{ - event::{Created, Event}, Id, + event::{Created, Event}, }; use crate::{clock::DateTime, name::Name}; @@ -6,10 +6,10 @@ use std::{future, io}; use axum::{ + Router, http::header, middleware, response::{IntoResponse, Response}, - Router, }; use clap::{CommandFactory, Parser}; use sqlx::sqlite::SqlitePool; diff --git a/src/clock.rs b/src/clock.rs index 242bcdf..4341266 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -1,6 +1,6 @@ use axum::{ extract::{Extension, FromRequestParts, Request}, - http::{request::Parts, StatusCode}, + http::{StatusCode, request::Parts}, middleware::Next, response::Response, }; @@ -21,7 +21,6 @@ impl RequestedAt { } } -#[async_trait::async_trait] impl<S> FromRequestParts<S> for RequestedAt where S: Send + Sync, diff --git a/src/db/backup.rs b/src/db/backup.rs index bb36aea..e3fa871 100644 --- a/src/db/backup.rs +++ b/src/db/backup.rs @@ -1,6 +1,6 @@ use rusqlite::{ - backup::{self}, Connection, + backup::{self}, }; use sqlx::sqlite::{LockedSqliteHandle, SqlitePool}; @@ -28,7 +28,7 @@ impl<'p> Backup<'p> { } } -impl<'p> Backup<'p> { +impl Backup<'_> { pub async fn backup(&mut self) -> Result<(), Error> { let mut to = self.to.acquire().await?; let mut to = Self::connection(&mut to.lock_handle().await?)?; diff --git a/src/event/app.rs b/src/event/app.rs index b309245..8661c90 100644 --- a/src/event/app.rs +++ b/src/event/app.rs @@ -1,12 +1,11 @@ use futures::{ - future, + Stream, future, stream::{self, StreamExt as _}, - Stream, }; use itertools::Itertools as _; use sqlx::sqlite::SqlitePool; -use super::{broadcaster::Broadcaster, Event, Sequence, Sequenced}; +use super::{Event, Sequence, Sequenced, broadcaster::Broadcaster}; use crate::{ channel::{self, repo::Provider as _}, login::{self, repo::Provider as _}, @@ -27,7 +26,7 @@ impl<'a> Events<'a> { pub async fn subscribe( &self, resume_at: Sequence, - ) -> Result<impl Stream<Item = Event> + std::fmt::Debug, Error> { + ) -> Result<impl Stream<Item = Event> + std::fmt::Debug + use<>, Error> { // Subscribe before retrieving, to catch messages broadcast while we're // querying the DB. We'll prune out duplicates later. let live_messages = self.events.subscribe(); @@ -76,7 +75,7 @@ impl<'a> Events<'a> { Ok(replay.chain(live_messages)) } - fn resume(resume_at: Sequence) -> impl for<'m> FnMut(&'m Event) -> future::Ready<bool> { + fn resume(resume_at: Sequence) -> impl for<'m> FnMut(&'m Event) -> future::Ready<bool> + use<> { let filter = Sequence::after(resume_at); move |event| future::ready(filter(event)) } diff --git a/src/event/extract.rs b/src/event/extract.rs index e3021e2..8fde1d5 100644 --- a/src/event/extract.rs +++ b/src/event/extract.rs @@ -1,11 +1,11 @@ use std::ops::Deref; use axum::{ - extract::FromRequestParts, - http::{request::Parts, HeaderName, HeaderValue}, + extract::{FromRequestParts, OptionalFromRequestParts}, + http::{HeaderName, HeaderValue, request::Parts}, }; use axum_extra::typed_header::TypedHeader; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; // A typed header. When used as a bare extractor, reads from the // `Last-Event-Id` HTTP header. @@ -44,7 +44,6 @@ where } } -#[async_trait::async_trait] impl<S, T> FromRequestParts<S> for LastEventId<T> where S: Send + Sync, @@ -53,12 +52,35 @@ where type Rejection = <TypedHeader<Self> as FromRequestParts<S>>::Rejection; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { - // This is purely for ergonomics: it allows `RequestedAt` to be extracted - // without having to wrap it in `Extension<>`. Callers _can_ still do that, + // This is purely for ergonomics: it allows `LastEventId` to be extracted + // without having to wrap it in `TypedHeader<>`. Callers _can_ still do that, // but they aren't forced to. - let TypedHeader(requested_at) = TypedHeader::from_request_parts(parts, state).await?; + let header = + <TypedHeader<Self> as FromRequestParts<S>>::from_request_parts(parts, state).await?; - Ok(requested_at) + Ok(header.into()) + } +} + +impl<S, T> OptionalFromRequestParts<S> for LastEventId<T> +where + S: Send + Sync, + T: Serialize + DeserializeOwned, +{ + type Rejection = <TypedHeader<Self> as FromRequestParts<S>>::Rejection; + + async fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> Result<Option<Self>, Self::Rejection> { + // This is purely for ergonomics: it allows `Option<LastEventId>` to be extracted + // without having to wrap it in `TypedHeader<>`. Callers _can_ still do that, + // but they aren't forced to. + let header = + <TypedHeader<Self> as OptionalFromRequestParts<S>>::from_request_parts(parts, state) + .await?; + + Ok(header.map(Self::from)) } } @@ -71,6 +93,13 @@ impl<T> Deref for LastEventId<T> { } } +impl<T> From<TypedHeader<LastEventId<T>>> for LastEventId<T> { + fn from(header: TypedHeader<Self>) -> Self { + let TypedHeader(value) = header; + value + } +} + impl<T> From<T> for LastEventId<T> { fn from(value: T) -> Self { Self(value) diff --git a/src/event/repo.rs b/src/event/repo.rs index 56beeea..ab3c449 100644 --- a/src/event/repo.rs +++ b/src/event/repo.rs @@ -1,4 +1,4 @@ -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; use crate::{ clock::DateTime, @@ -9,7 +9,7 @@ pub trait Provider { fn sequence(&mut self) -> Sequences; } -impl<'c> Provider for Transaction<'c, Sqlite> { +impl Provider for Transaction<'_, Sqlite> { fn sequence(&mut self) -> Sequences { Sequences(self) } @@ -17,7 +17,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Sequences<'t>(&'t mut SqliteConnection); -impl<'c> Sequences<'c> { +impl Sequences<'_> { pub async fn next(&mut self, at: &DateTime) -> Result<Instant, sqlx::Error> { let next = sqlx::query_scalar!( r#" diff --git a/src/event/routes/get.rs b/src/event/routes/get.rs index ceebcc9..2ca8991 100644 --- a/src/event/routes/get.rs +++ b/src/event/routes/get.rs @@ -1,9 +1,8 @@ use axum::{ extract::State, response::{ - self, + self, IntoResponse, sse::{self, Sse}, - IntoResponse, }, }; use axum_extra::extract::Query; @@ -12,7 +11,7 @@ use futures::stream::{Stream, StreamExt as _}; use crate::{ app::App, error::{Internal, Unauthorized}, - event::{app, extract::LastEventId, Event, Sequence, Sequenced as _}, + event::{Event, Sequence, Sequenced as _, app, extract::LastEventId}, token::{app::ValidateError, extract::Identity}, }; diff --git a/src/event/routes/mod.rs b/src/event/routes/mod.rs index 57ab9db..742d397 100644 --- a/src/event/routes/mod.rs +++ b/src/event/routes/mod.rs @@ -1,4 +1,4 @@ -use axum::{routing::get, Router}; +use axum::{Router, routing::get}; use crate::app::App; diff --git a/src/event/routes/test/resume.rs b/src/event/routes/test/resume.rs index fabda0c..dc27691 100644 --- a/src/event/routes/test/resume.rs +++ b/src/event/routes/test/resume.rs @@ -5,7 +5,7 @@ use axum_extra::extract::Query; use futures::stream::{self, StreamExt as _}; use crate::{ - event::{routes::get, Sequenced as _}, + event::{Sequenced as _, routes::get}, test::fixtures::{self, future::Expect as _}, }; @@ -132,9 +132,11 @@ async fn serial_resume() { .expect_ready("zipping a finite list of events is ready immediately") .await; - assert!(events - .iter() - .all(|(event, message)| message == &event.message)); + assert!( + events + .iter() + .all(|(event, message)| message == &event.message) + ); let (event, _) = events.last().expect("this vec is non-empty"); @@ -173,9 +175,11 @@ async fn serial_resume() { .expect_ready("zipping a finite list of events is ready immediately") .await; - assert!(events - .iter() - .all(|(event, message)| message == &event.message)); + assert!( + events + .iter() + .all(|(event, message)| message == &event.message) + ); let (event, _) = events.last().expect("this vec is non-empty"); @@ -214,8 +218,10 @@ async fn serial_resume() { .expect_ready("zipping a finite list of events is ready immediately") .await; - assert!(events - .iter() - .all(|(event, message)| message == &event.message)); + assert!( + events + .iter() + .all(|(event, message)| message == &event.message) + ); }; } diff --git a/src/invite/app.rs b/src/invite/app.rs index d4e877a..c56c9b3 100644 --- a/src/invite/app.rs +++ b/src/invite/app.rs @@ -1,17 +1,17 @@ use chrono::TimeDelta; use sqlx::sqlite::SqlitePool; -use super::{repo::Provider as _, Id, Invite, Summary}; +use super::{Id, Invite, Summary, repo::Provider as _}; use crate::{ clock::DateTime, db::{Duplicate as _, NotFound as _}, event::Broadcaster, login::{ - create::{self, Create}, Login, Password, + create::{self, Create}, }, name::Name, - token::{repo::Provider as _, Secret}, + token::{Secret, repo::Provider as _}, }; pub struct Invites<'a> { diff --git a/src/invite/repo.rs b/src/invite/repo.rs index 5f86e49..c1dc701 100644 --- a/src/invite/repo.rs +++ b/src/invite/repo.rs @@ -1,4 +1,4 @@ -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; use super::{Id, Invite, Summary}; use crate::{ @@ -11,7 +11,7 @@ pub trait Provider { fn invites(&mut self) -> Invites; } -impl<'c> Provider for Transaction<'c, Sqlite> { +impl Provider for Transaction<'_, Sqlite> { fn invites(&mut self) -> Invites { Invites(self) } @@ -19,7 +19,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Invites<'t>(&'t mut SqliteConnection); -impl<'c> Invites<'c> { +impl Invites<'_> { pub async fn create( &mut self, issuer: &Login, diff --git a/src/invite/routes/mod.rs b/src/invite/routes/mod.rs index 2f7375c..d83efc6 100644 --- a/src/invite/routes/mod.rs +++ b/src/invite/routes/mod.rs @@ -1,6 +1,6 @@ use axum::{ - routing::{get, post}, Router, + routing::{get, post}, }; use crate::app::App; @@ -13,6 +13,6 @@ mod test; pub fn router() -> Router<App> { Router::new() .route("/api/invite", post(post::handler)) - .route("/api/invite/:invite", get(invite::get::handler)) - .route("/api/invite/:invite", post(invite::post::handler)) + .route("/api/invite/{invite}", get(invite::get::handler)) + .route("/api/invite/{invite}", post(invite::post::handler)) } diff --git a/src/login/app.rs b/src/login/app.rs index f458561..2da4d6a 100644 --- a/src/login/app.rs +++ b/src/login/app.rs @@ -1,8 +1,8 @@ use sqlx::sqlite::SqlitePool; use super::{ - create::{self, Create}, Login, Password, + create::{self, Create}, }; use crate::{clock::DateTime, event::Broadcaster, name::Name}; diff --git a/src/login/create.rs b/src/login/create.rs index 693daaf..c5cea08 100644 --- a/src/login/create.rs +++ b/src/login/create.rs @@ -7,6 +7,7 @@ use crate::{ name::Name, }; +#[must_use = "dropping a login creation attempt is likely a mistake"] pub struct Create<'a> { name: &'a Name, password: &'a Password, @@ -14,7 +15,6 @@ pub struct Create<'a> { } impl<'a> Create<'a> { - #[must_use = "dropping a login creation attempt is likely a mistake"] pub fn begin(name: &'a Name, password: &'a Password, created_at: &'a DateTime) -> Self { Self { name, @@ -23,7 +23,6 @@ impl<'a> Create<'a> { } } - #[must_use = "dropping a login creation attempt is likely a mistake"] pub fn validate(self) -> Result<Validated<'a>, Error> { let Self { name, @@ -45,15 +44,15 @@ impl<'a> Create<'a> { } } +#[must_use = "dropping a login creation attempt is likely a mistake"] pub struct Validated<'a> { name: &'a Name, password_hash: StoredHash, created_at: &'a DateTime, } -impl<'a> Validated<'a> { - #[must_use = "dropping a login creation attempt is likely a mistake"] - pub async fn store<'c>(self, tx: &mut Transaction<'c, Sqlite>) -> Result<Stored, sqlx::Error> { +impl Validated<'_> { + pub async fn store(self, tx: &mut Transaction<'_, Sqlite>) -> Result<Stored, sqlx::Error> { let Self { name, password_hash, @@ -67,6 +66,7 @@ impl<'a> Validated<'a> { } } +#[must_use = "dropping a login creation attempt is likely a mistake"] pub struct Stored { login: History, } diff --git a/src/login/history.rs b/src/login/history.rs index 8161b0b..d67bcce 100644 --- a/src/login/history.rs +++ b/src/login/history.rs @@ -1,6 +1,6 @@ use super::{ - event::{Created, Event}, Id, Login, + event::{Created, Event}, }; use crate::event::{Instant, Sequence}; @@ -46,7 +46,7 @@ impl History { .into() } - pub fn events(&self) -> impl Iterator<Item = Event> { + pub fn events(&self) -> impl Iterator<Item = Event> + use<> { [self.created()].into_iter() } } diff --git a/src/login/repo.rs b/src/login/repo.rs index 1c63a4b..03f2c17 100644 --- a/src/login/repo.rs +++ b/src/login/repo.rs @@ -1,10 +1,10 @@ use futures::stream::{StreamExt as _, TryStreamExt as _}; -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; use crate::{ clock::DateTime, event::{Instant, Sequence}, - login::{password::StoredHash, History, Id, Login}, + login::{History, Id, Login, password::StoredHash}, name::{self, Name}, }; @@ -12,7 +12,7 @@ pub trait Provider { fn logins(&mut self) -> Logins; } -impl<'c> Provider for Transaction<'c, Sqlite> { +impl Provider for Transaction<'_, Sqlite> { fn logins(&mut self) -> Logins { Logins(self) } @@ -20,7 +20,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Logins<'t>(&'t mut SqliteConnection); -impl<'c> Logins<'c> { +impl Logins<'_> { pub async fn create( &mut self, name: &Name, diff --git a/src/login/routes/mod.rs b/src/login/routes/mod.rs index bbd0c3f..ade96cb 100644 --- a/src/login/routes/mod.rs +++ b/src/login/routes/mod.rs @@ -1,4 +1,4 @@ -use axum::{routing::post, Router}; +use axum::{Router, routing::post}; use crate::app::App; diff --git a/src/login/snapshot.rs b/src/login/snapshot.rs index e1eb96c..5c5dce0 100644 --- a/src/login/snapshot.rs +++ b/src/login/snapshot.rs @@ -1,6 +1,6 @@ use super::{ - event::{Created, Event}, Id, + event::{Created, Event}, }; use crate::name::Name; diff --git a/src/message/app.rs b/src/message/app.rs index 7bf68d1..60206f1 100644 --- a/src/message/app.rs +++ b/src/message/app.rs @@ -2,12 +2,12 @@ use chrono::TimeDelta; use itertools::Itertools; use sqlx::sqlite::SqlitePool; -use super::{repo::Provider as _, Body, Id, Message}; +use super::{Body, Id, Message, repo::Provider as _}; use crate::{ channel::{self, repo::Provider as _}, clock::DateTime, db::NotFound as _, - event::{repo::Provider as _, Broadcaster, Event, Sequence}, + event::{Broadcaster, Event, Sequence, repo::Provider as _}, login::Login, name, }; diff --git a/src/message/event.rs b/src/message/event.rs index 1cd5847..bd116b1 100644 --- a/src/message/event.rs +++ b/src/message/event.rs @@ -1,4 +1,4 @@ -use super::{snapshot::Message, Id}; +use super::{Id, snapshot::Message}; use crate::event::{Instant, Sequenced}; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] diff --git a/src/message/history.rs b/src/message/history.rs index ed8f5df..1a72c08 100644 --- a/src/message/history.rs +++ b/src/message/history.rs @@ -1,8 +1,8 @@ use itertools::Itertools as _; use super::{ - event::{Deleted, Event, Sent}, Id, Message, + event::{Deleted, Event, Sent}, }; use crate::event::{Instant, Sequence}; @@ -58,7 +58,7 @@ impl History { }) } - pub fn events(&self) -> impl Iterator<Item = Event> { + pub fn events(&self) -> impl Iterator<Item = Event> + use<> { [self.sent()] .into_iter() .merge_by(self.deleted(), Sequence::merge) diff --git a/src/message/repo.rs b/src/message/repo.rs index 14f8eaf..8a0a72c 100644 --- a/src/message/repo.rs +++ b/src/message/repo.rs @@ -1,6 +1,6 @@ -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; -use super::{snapshot::Message, Body, History, Id}; +use super::{Body, History, Id, snapshot::Message}; use crate::{ channel, clock::DateTime, @@ -12,7 +12,7 @@ pub trait Provider { fn messages(&mut self) -> Messages; } -impl<'c> Provider for Transaction<'c, Sqlite> { +impl Provider for Transaction<'_, Sqlite> { fn messages(&mut self) -> Messages { Messages(self) } @@ -20,7 +20,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Messages<'t>(&'t mut SqliteConnection); -impl<'c> Messages<'c> { +impl Messages<'_> { pub async fn create( &mut self, channel: &channel::History, diff --git a/src/message/routes/mod.rs b/src/message/routes/mod.rs index dfe8628..00b2b1a 100644 --- a/src/message/routes/mod.rs +++ b/src/message/routes/mod.rs @@ -1,9 +1,9 @@ -use axum::{routing::delete, Router}; +use axum::{Router, routing::delete}; use crate::app::App; mod message; pub fn router() -> Router<App> { - Router::new().route("/api/messages/:message", delete(message::delete::handler)) + Router::new().route("/api/messages/{message}", delete(message::delete::handler)) } diff --git a/src/message/snapshot.rs b/src/message/snapshot.rs index 53b7176..d924ea1 100644 --- a/src/message/snapshot.rs +++ b/src/message/snapshot.rs @@ -1,6 +1,6 @@ use super::{ - event::{Event, Sent}, Body, Id, + event::{Event, Sent}, }; use crate::{channel, clock::DateTime, event::Instant, login}; diff --git a/src/name.rs b/src/name.rs index 9187d33..9861bc1 100644 --- a/src/name.rs +++ b/src/name.rs @@ -50,7 +50,7 @@ impl Name { #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("stored canonical form {0:#?} does not match computed canonical form {:#?} for name {:#?}", .1.as_str(), .2.as_str())] + #[error("stored canonical form {stored:#?} does not match computed canonical form {computed:#?} for name {expected:#?}", stored=.0, computed=.1.as_str(), expected=.2.as_str())] CanonicalMismatch(String, ident::String, nfc::String), } diff --git a/src/normalize/string.rs b/src/normalize/string.rs index a0d178c..ee33267 100644 --- a/src/normalize/string.rs +++ b/src/normalize/string.rs @@ -1,8 +1,8 @@ use std::{fmt, string::String as StdString}; use sqlx::{ - encode::{Encode, IsNull}, - Database, Decode, Type, + encode::{Encode, IsNull}, Database, Decode, + Type, }; pub trait Normalize: Clone + Default { @@ -84,20 +84,20 @@ where DB: Database, StdString: Encode<'q, DB>, { - fn encode_by_ref( - &self, + fn encode( + self, buf: &mut <DB as Database>::ArgumentBuffer<'q>, ) -> Result<IsNull, sqlx::error::BoxDynError> { let Self(value, _) = self; - value.encode_by_ref(buf) + value.encode(buf) } - fn encode( - self, + fn encode_by_ref( + &self, buf: &mut <DB as Database>::ArgumentBuffer<'q>, ) -> Result<IsNull, sqlx::error::BoxDynError> { let Self(value, _) = self; - value.encode(buf) + value.encode_by_ref(buf) } fn produces(&self) -> Option<<DB as Database>::TypeInfo> { diff --git a/src/setup/app.rs b/src/setup/app.rs index c1f7b69..9553f40 100644 --- a/src/setup/app.rs +++ b/src/setup/app.rs @@ -5,11 +5,11 @@ use crate::{ clock::DateTime, event::Broadcaster, login::{ - create::{self, Create}, Login, Password, + create::{self, Create}, }, name::Name, - token::{repo::Provider as _, Secret}, + token::{Secret, repo::Provider as _}, }; pub struct Setup<'a> { diff --git a/src/setup/repo.rs b/src/setup/repo.rs index de93f51..ac01496 100644 --- a/src/setup/repo.rs +++ b/src/setup/repo.rs @@ -1,10 +1,10 @@ -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; pub trait Provider { fn setup(&mut self) -> Setup; } -impl<'c> Provider for Transaction<'c, Sqlite> { +impl Provider for Transaction<'_, Sqlite> { fn setup(&mut self) -> Setup { Setup(self) } @@ -12,7 +12,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Setup<'t>(&'t mut SqliteConnection); -impl<'c> Setup<'c> { +impl Setup<'_> { pub async fn completed(&mut self) -> Result<bool, sqlx::Error> { let completed = sqlx::query_scalar!( r#" diff --git a/src/setup/routes/mod.rs b/src/setup/routes/mod.rs index 6054983..977a790 100644 --- a/src/setup/routes/mod.rs +++ b/src/setup/routes/mod.rs @@ -1,4 +1,4 @@ -use axum::{routing::post, Router}; +use axum::{Router, routing::post}; use crate::app::App; diff --git a/src/test/fixtures/cookie.rs b/src/test/fixtures/cookie.rs index 58777c8..fcb379f 100644 --- a/src/test/fixtures/cookie.rs +++ b/src/test/fixtures/cookie.rs @@ -5,7 +5,7 @@ use crate::{ clock::RequestedAt, login::Password, name::Name, - token::{extract::IdentityCookie, Secret}, + token::{Secret, extract::IdentityCookie}, }; pub fn not_logged_in() -> IdentityCookie { diff --git a/src/test/fixtures/event.rs b/src/test/fixtures/event.rs index de02d4d..e11f6ee 100644 --- a/src/test/fixtures/event.rs +++ b/src/test/fixtures/event.rs @@ -26,8 +26,8 @@ pub fn login(event: Event) -> Ready<Option<login::Event>> { pub mod channel { use std::future::{self, Ready}; - use crate::channel::event; pub use crate::channel::Event; + use crate::channel::event; pub fn created(event: Event) -> Ready<Option<event::Created>> { future::ready(match event { @@ -47,8 +47,8 @@ pub mod channel { pub mod message { use std::future::{self, Ready}; - use crate::message::event; pub use crate::message::Event; + use crate::message::event; pub fn sent(event: Event) -> Ready<Option<event::Sent>> { future::ready(match event { @@ -68,8 +68,8 @@ pub mod message { pub mod login { use std::future::{self, Ready}; - use crate::login::event; pub use crate::login::Event; + use crate::login::event; pub fn created(event: Event) -> Ready<Option<event::Created>> { future::ready(match event { diff --git a/src/test/fixtures/future.rs b/src/test/fixtures/future.rs index 2f810a3..c0fa528 100644 --- a/src/test/fixtures/future.rs +++ b/src/test/fixtures/future.rs @@ -52,7 +52,7 @@ pub trait Expect: Sized { Self: Future<Output = Option<T>>; } -impl<'a, St> Expect for stream::Next<'a, St> { +impl<St> Expect for stream::Next<'_, St> { fn expect_ready(self, message: &str) -> Ready<Self> { Ready { future: self, @@ -131,7 +131,7 @@ pub struct Ready<'m, F> { message: &'m str, } -impl<'m, F> Future for Ready<'m, F> +impl<F> Future for Ready<'_, F> where F: Future + std::fmt::Debug, { @@ -155,7 +155,7 @@ pub struct Wait<'m, F> { message: &'m str, } -impl<'m, F> Future for Wait<'m, F> +impl<F> Future for Wait<'_, F> where F: Future + std::fmt::Debug, { @@ -179,7 +179,7 @@ pub struct Some<'m, F> { message: &'m str, } -impl<'m, F, T> Future for Some<'m, F> +impl<F, T> Future for Some<'_, F> where F: Future<Output = Option<T>> + std::fmt::Debug, { @@ -203,7 +203,7 @@ pub struct None<'m, F> { message: &'m str, } -impl<'m, F, T> Future for None<'m, F> +impl<F, T> Future for None<'_, F> where F: Future<Output = Option<T>> + std::fmt::Debug, { diff --git a/src/token/app.rs b/src/token/app.rs index 5c0aeb0..3f054ff 100644 --- a/src/token/app.rs +++ b/src/token/app.rs @@ -1,19 +1,18 @@ use chrono::TimeDelta; use futures::{ - future, + Stream, future, stream::{self, StreamExt as _}, - Stream, }; use sqlx::sqlite::SqlitePool; use super::{ - repo::{self, auth::Provider as _, Provider as _}, Broadcaster, Event as TokenEvent, Id, Secret, + repo::{self, Provider as _, auth::Provider as _}, }; use crate::{ clock::DateTime, db::NotFound as _, - login::{repo::Provider as _, Login, Password}, + login::{Login, Password, repo::Provider as _}, name::{self, Name}, }; @@ -120,12 +119,13 @@ impl<'a> Tokens<'a> { Ok((token, login)) } - pub async fn limit_stream<E>( + pub async fn limit_stream<S, E>( &self, token: Id, - events: impl Stream<Item = E> + std::fmt::Debug, - ) -> Result<impl Stream<Item = E> + std::fmt::Debug, ValidateError> + events: S, + ) -> Result<impl Stream<Item = E> + std::fmt::Debug + use<S, E>, ValidateError> where + S: Stream<Item = E> + std::fmt::Debug, E: std::fmt::Debug, { // Subscribe, first. diff --git a/src/token/extract/cookie.rs b/src/token/extract/cookie.rs index af5787d..a8e15d4 100644 --- a/src/token/extract/cookie.rs +++ b/src/token/extract/cookie.rs @@ -25,7 +25,7 @@ impl fmt::Debug for Identity { } impl Identity { - const COOKIE_NAME: &str = "identity"; + const COOKIE_NAME: &'static str = "identity"; // Creates a new, unpopulated identity token store. #[cfg(test)] @@ -71,7 +71,6 @@ impl Identity { } } -#[async_trait::async_trait] impl<S> FromRequestParts<S> for Identity where S: Send + Sync, diff --git a/src/token/extract/identity.rs b/src/token/extract/identity.rs index a69f509..acfd7ae 100644 --- a/src/token/extract/identity.rs +++ b/src/token/extract/identity.rs @@ -1,5 +1,5 @@ use axum::{ - extract::{FromRequestParts, State}, + extract::{FromRequestParts, OptionalFromRequestParts, State}, http::request::Parts, response::{IntoResponse, Response}, }; @@ -20,7 +20,6 @@ pub struct Identity { pub login: Login, } -#[async_trait::async_trait] impl FromRequestParts<App> for Identity { type Rejection = LoginError<Internal>; @@ -39,6 +38,21 @@ impl FromRequestParts<App> for Identity { } } +impl OptionalFromRequestParts<App> for Identity { + type Rejection = LoginError<Internal>; + + async fn from_request_parts( + parts: &mut Parts, + state: &App, + ) -> Result<Option<Self>, Self::Rejection> { + match <Self as FromRequestParts<App>>::from_request_parts(parts, state).await { + Ok(identity) => Ok(Some(identity)), + Err(LoginError::Unauthorized) => Ok(None), + Err(other) => Err(other), + } + } +} + pub enum LoginError<E> { Failure(E), Unauthorized, diff --git a/src/token/repo/auth.rs b/src/token/repo/auth.rs index b51db8c..0deed10 100644 --- a/src/token/repo/auth.rs +++ b/src/token/repo/auth.rs @@ -1,10 +1,10 @@ -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; use crate::{ clock::DateTime, db::NotFound, event::{Instant, Sequence}, - login::{self, password::StoredHash, History, Login}, + login::{self, History, Login, password::StoredHash}, name::{self, Name}, }; @@ -12,7 +12,7 @@ pub trait Provider { fn auth(&mut self) -> Auth; } -impl<'c> Provider for Transaction<'c, Sqlite> { +impl Provider for Transaction<'_, Sqlite> { fn auth(&mut self) -> Auth { Auth(self) } @@ -20,7 +20,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Auth<'t>(&'t mut SqliteConnection); -impl<'t> Auth<'t> { +impl Auth<'_> { pub async fn for_name(&mut self, name: &Name) -> Result<(History, StoredHash), LoadError> { let name = name.canonical(); let row = sqlx::query!( diff --git a/src/token/repo/token.rs b/src/token/repo/token.rs index 33b89d5..ff42fad 100644 --- a/src/token/repo/token.rs +++ b/src/token/repo/token.rs @@ -1,4 +1,4 @@ -use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; +use sqlx::{SqliteConnection, Transaction, sqlite::Sqlite}; use uuid::Uuid; use crate::{ @@ -14,7 +14,7 @@ pub trait Provider { fn tokens(&mut self) -> Tokens; } -impl<'c> Provider for Transaction<'c, Sqlite> { +impl Provider for Transaction<'_, Sqlite> { fn tokens(&mut self) -> Tokens { Tokens(self) } @@ -22,7 +22,7 @@ impl<'c> Provider for Transaction<'c, Sqlite> { pub struct Tokens<'t>(&'t mut SqliteConnection); -impl<'c> Tokens<'c> { +impl Tokens<'_> { // Issue a new token for an existing login. The issued_at timestamp will // be used to control expiry, until the token is actually used. pub async fn issue( diff --git a/src/ui/assets.rs b/src/ui/assets.rs index 6a7563a..142cdf9 100644 --- a/src/ui/assets.rs +++ b/src/ui/assets.rs @@ -1,6 +1,6 @@ use ::mime::{FromStrError, Mime}; use axum::{ - http::{header, StatusCode}, + http::{StatusCode, header}, response::{IntoResponse, Response}, }; use rust_embed::EmbeddedFile; diff --git a/src/ui/routes/mod.rs b/src/ui/routes/mod.rs index 48b3f90..80dc1e5 100644 --- a/src/ui/routes/mod.rs +++ b/src/ui/routes/mod.rs @@ -1,4 +1,4 @@ -use axum::{middleware, routing::get, Router}; +use axum::{Router, middleware, routing::get}; use crate::{app::App, ui::middleware::setup_required}; @@ -13,14 +13,14 @@ mod setup; pub fn router(app: &App) -> Router<App> { [ Router::new() - .route("/*path", get(path::get::handler)) + .route("/{*path}", get(path::get::handler)) .route("/setup", get(setup::get::handler)), Router::new() .route("/", get(get::handler)) .route("/me", get(me::get::handler)) .route("/login", get(login::get::handler)) - .route("/ch/:channel", get(ch::channel::get::handler)) - .route("/invite/:invite", get(invite::invite::get::handler)) + .route("/ch/{channel}", get(ch::channel::get::handler)) + .route("/invite/{invite}", get(invite::invite::get::handler)) .route_layer(middleware::from_fn_with_state(app.clone(), setup_required)), ] .into_iter() @@ -14,7 +14,7 @@ body { background-color: var(--colour-active-channel-bg); color: var(--dark-text); - font-family: 'Roboto'; + font-family: 'Roboto', sans-serif; } hr { diff --git a/ui/lib/apiServer.js b/ui/lib/apiServer.js index e52daff..c65b743 100644 --- a/ui/lib/apiServer.js +++ b/ui/lib/apiServer.js @@ -1,5 +1,4 @@ import axios from 'axios'; -import { channelsList, logins, messages } from '$lib/store'; export const apiServer = axios.create({ baseURL: '/api/', @@ -55,75 +54,11 @@ export async function acceptInvite(inviteId, username, password) { } export function subscribeToEvents(resumePoint) { - const eventsUrl = new URL('/api/events', window.location); - eventsUrl.searchParams.append('resume_point', resumePoint); - const evtSource = new EventSource(eventsUrl.toString()); - // TODO: this should process all incoming events and store them. - // TODO: eventually we'll need to handle expiring old info, so as not to use - // infinite browser memory. - /* - * Known message types as of now: - * - created: a channel is created. - * - action: ignore. - * - message: a message is created. - * - action: display message in channel. - * - message_deleted: a message is deleted. - * - action: replace message with <...>. - * - deleted: a channel is deleted. - * - action: remove channel from sidebar. - */ - evtSource.onmessage = (evt) => { - const data = JSON.parse(evt.data); - - switch (data.type) { - case 'login': - onLoginEvent(data); - break; - case 'channel': - onChannelEvent(data); - break; - case 'message': - onMessageEvent(data); - break; + const eventsUrl = apiServer.getUri({ + url: '/events', + params: { + resume_point: resumePoint } - }; - - return evtSource; -} - -function onLoginEvent(data) { - switch (data.event) { - case 'created': - logins.update((value) => value.addLogin(data.id, data.name)); - break; - } -} - -function onChannelEvent(data) { - switch (data.event) { - case 'created': - channelsList.update((value) => value.addChannel(data.id, data.name)); - break; - case 'deleted': - channelsList.update((value) => value.deleteChannel(data.id)); - messages.update((value) => value.deleteChannel(data.id)); - break; - } -} - -function onMessageEvent(data) { - switch (data.event) { - case 'sent': - messages.update((value) => - value.addMessage(data.channel, data.id, { - at: data.at, - sender: data.sender, - body: data.body - }) - ); - break; - case 'deleted': - messages.update((value) => value.deleteMessage(data.id)); - break; - } + }); + return new EventSource(eventsUrl); } diff --git a/ui/lib/components/Channel.svelte b/ui/lib/components/Channel.svelte index c73340f..4f908d2 100644 --- a/ui/lib/components/Channel.svelte +++ b/ui/lib/components/Channel.svelte @@ -2,13 +2,13 @@ let { id, name, active, hasUnreads } = $props(); </script> -<a href="/ch/{id}"> - <li class:active> +<li class:active> + <a href="/ch/{id}"> {#if hasUnreads} <span class="badge has-unreads">❦</span> {:else} <span class="badge has-no-unreads">¶</span> {/if} <span>{name}</span> - </li> -</a> + </a> +</li> diff --git a/ui/lib/components/LogOut.svelte b/ui/lib/components/LogOut.svelte index b699cfd..1cb8fb5 100644 --- a/ui/lib/components/LogOut.svelte +++ b/ui/lib/components/LogOut.svelte @@ -8,7 +8,7 @@ const response = await logOut(); if (200 <= response.status && response.status < 300) { currentUser.set(null); - goto('/login'); + await goto('/login'); } } </script> diff --git a/ui/lib/components/MessageInput.svelte b/ui/lib/components/MessageInput.svelte index 5869654..1eb1d7b 100644 --- a/ui/lib/components/MessageInput.svelte +++ b/ui/lib/components/MessageInput.svelte @@ -24,7 +24,6 @@ </script> <form bind:this={form} onsubmit={onSubmit}> - <textarea onkeydown={onKeyDown} bind:value {disabled} type="search" placeholder="Say something..." - ></textarea> + <textarea onkeydown={onKeyDown} bind:value {disabled} placeholder="Say something..."></textarea> <button type="submit">»</button> </form> diff --git a/ui/lib/store.js b/ui/lib/store.js index 508320f..afced4c 100644 --- a/ui/lib/store.js +++ b/ui/lib/store.js @@ -15,3 +15,64 @@ export const logins = writable(new Logins()); export const channelsMetaList = writable(new ChannelsMeta({ channelsMetaData })); export const channelsList = writable(new Channels({ channelsMetaList })); export const messages = writable(new Messages()); + +export function onEvent(event) { + switch (event.type) { + case 'login': + onLoginEvent(event); + break; + case 'channel': + onChannelEvent(event); + break; + case 'message': + onMessageEvent(event); + break; + } +} + +onEvent.fromJson = (event) => { + const parsed = JSON.parse(event); + return onEvent(parsed); +}; + +onEvent.fromMessage = (message) => { + const data = message.data; + return onEvent.fromJson(data); +}; + +function onLoginEvent(event) { + switch (event.event) { + case 'created': + logins.update((value) => value.addLogin(event.id, event.name)); + break; + } +} + +function onChannelEvent(event) { + switch (event.event) { + case 'created': + channelsList.update((value) => value.addChannel(event.id, event.name)); + break; + case 'deleted': + channelsList.update((value) => value.deleteChannel(event.id)); + messages.update((value) => value.deleteChannel(event.id)); + break; + } +} + +function onMessageEvent(event) { + switch (event.event) { + case 'sent': + messages.update((value) => + value.addMessage(event.channel, event.id, { + at: event.at, + sender: event.sender, + body: event.body + }) + ); + break; + case 'deleted': + messages.update((value) => value.deleteMessage(event.id)); + break; + } +} diff --git a/ui/routes/(app)/+layout.svelte b/ui/routes/(app)/+layout.svelte index 9ade399..888a185 100644 --- a/ui/routes/(app)/+layout.svelte +++ b/ui/routes/(app)/+layout.svelte @@ -1,12 +1,12 @@ <script> - import { page } from '$app/stores'; + import { page } from '$app/state'; import { goto } from '$app/navigation'; import { browser } from '$app/environment'; - import { onMount, onDestroy, getContext } from 'svelte'; + import { getContext, onDestroy, onMount } from 'svelte'; import TinyGesture from 'tinygesture'; import { boot, subscribeToEvents } from '$lib/apiServer'; - import { currentUser, logins, channelsList, channelsMetaList, messages } from '$lib/store'; + import { channelsList, channelsMetaList, currentUser, logins, messages, onEvent } from '$lib/store'; import ChannelList from '$lib/components/ChannelList.svelte'; import CreateChannelForm from '$lib/components/CreateChannelForm.svelte'; @@ -17,20 +17,11 @@ let pageContext = getContext('page'); let { children } = $props(); let loading = $state(true); - let channel = $derived($page.params.channel); + let channel = $derived(page.params.channel); - let rawChannels; - channelsList.subscribe((val) => { - rawChannels = val.channels; - }); - let rawChannelsMeta; - channelsMetaList.subscribe((val) => { - rawChannelsMeta = val.channelsMeta; - }); - let rawMessages; - messages.subscribe((val) => { - rawMessages = val; - }); + let rawChannels = $derived($channelsList.channels); + let rawChannelsMeta = $derived($channelsMetaList.channelsMeta); + let rawMessages = $derived($messages); let enrichedChannels = $derived.by(() => { const channels = rawChannels; @@ -84,14 +75,15 @@ case 200: onBooted(response.data); events = subscribeToEvents(response.data.resume_point); + events.onmessage = onEvent.fromMessage; break; case 401: currentUser.set(null); - goto('/login'); + await goto('/login'); break; case 503: currentUser.set(null); - goto('/setup'); + await goto('/setup'); break; default: // TODO: display error. @@ -111,18 +103,18 @@ } }); - function beforeUnload(evt) { - evt.preventDefault(); + function onbeforeunload(event) { + event.preventDefault(); if (events !== null) { events.close(); } // For some compat reasons? - evt.returnValue = ''; + event.returnValue = ''; return ''; } </script> -<svelte:window on:beforeunload={beforeUnload} /> +<svelte:window {onbeforeunload} /> <svelte:head> <!-- TODO: unread count? --> diff --git a/ui/routes/(app)/ch/[channel]/+page.svelte b/ui/routes/(app)/ch/[channel]/+page.svelte index 25bc318..54ebda7 100644 --- a/ui/routes/(app)/ch/[channel]/+page.svelte +++ b/ui/routes/(app)/ch/[channel]/+page.svelte @@ -1,11 +1,11 @@ <script> import { DateTime } from 'luxon'; - import { page } from '$app/stores'; + import { page } from '$app/state'; import ActiveChannel from '$lib/components/ActiveChannel.svelte'; import MessageInput from '$lib/components/MessageInput.svelte'; import { channelsMetaList, messages } from '$lib/store'; - let channel = $derived($page.params.channel); + let channel = $derived(page.params.channel); let messageRuns = $derived($messages.inChannel(channel)); let activeChannel; @@ -53,7 +53,8 @@ } let lastReadCallback = null; - function handleScroll() { + + function onscroll() { clearTimeout(lastReadCallback); // Fine if lastReadCallback is null still. lastReadCallback = setTimeout(setLastRead, 2 * 1000); } @@ -61,7 +62,7 @@ <svelte:window onkeydown={handleKeydown} /> -<div class="active-channel" on:scroll={handleScroll} bind:this={activeChannel}> +<div class="active-channel" {onscroll} bind:this={activeChannel}> <ActiveChannel {messageRuns} /> </div> <div class="create-message"> diff --git a/ui/routes/(login)/invite/[invite]/+page.svelte b/ui/routes/(login)/invite/[invite]/+page.svelte index 132cbc1..0c01286 100644 --- a/ui/routes/(login)/invite/[invite]/+page.svelte +++ b/ui/routes/(login)/invite/[invite]/+page.svelte @@ -18,7 +18,7 @@ if (200 <= response.status && response.status < 300) { username = ''; password = ''; - goto('/'); + await goto('/'); } pending = false; } diff --git a/ui/routes/(login)/login/+page.svelte b/ui/routes/(login)/login/+page.svelte index a1291ea..9157cef 100644 --- a/ui/routes/(login)/login/+page.svelte +++ b/ui/routes/(login)/login/+page.svelte @@ -16,7 +16,7 @@ if (200 <= response.status && response.status < 300) { username = ''; password = ''; - goto('/'); + await goto('/'); } pending = false; } diff --git a/ui/routes/(login)/setup/+page.svelte b/ui/routes/(login)/setup/+page.svelte index f162ded..c63f198 100644 --- a/ui/routes/(login)/setup/+page.svelte +++ b/ui/routes/(login)/setup/+page.svelte @@ -16,7 +16,7 @@ if (200 <= response.status && response.status < 300) { username = ''; password = ''; - goto('/'); + await goto('/'); } pending = false; } diff --git a/ui/styles/app-bar.css b/ui/styles/app-bar.css index 17620ba..0d0a311 100644 --- a/ui/styles/app-bar.css +++ b/ui/styles/app-bar.css @@ -28,7 +28,7 @@ .app-bar > a { line-height: var(--app-bar-height); - font-family: 'Archistico'; + font-family: 'Archistico', serif; letter-spacing: 0.25rem; } diff --git a/ui/styles/fonts.css b/ui/styles/fonts.css index 06f69c8..280f8c6 100644 --- a/ui/styles/fonts.css +++ b/ui/styles/fonts.css @@ -6,6 +6,7 @@ font-style: normal; font-display: swap; } + @font-face { font-family: 'Roboto'; src: url('../fonts/Roboto-Bold.ttf') format('truetype'); @@ -13,6 +14,7 @@ font-style: normal; font-display: swap; } + @font-face { font-family: 'Roboto'; src: url('../fonts/Roboto-Italic.ttf') format('truetype'); @@ -20,6 +22,7 @@ font-style: italic; font-display: swap; } + @font-face { font-family: 'Roboto'; src: url('../fonts/Roboto-BoldItalic.ttf') format('truetype'); @@ -36,6 +39,7 @@ font-style: normal; font-display: swap; } + @font-face { font-family: 'Archistico'; src: url('../fonts/Archistico_Bold.ttf') format('truetype'); @@ -52,6 +56,7 @@ font-style: normal; font-display: swap; } + @font-face { font-family: 'FiraCode'; src: url('../fonts/FiraCode-Bold.otf') format('opentype'); @@ -59,20 +64,6 @@ font-style: normal; font-display: swap; } -@font-face { - font-family: 'FiraCode'; - src: url('../fonts/FiraCode-Italic.otf') format('opentype'); - font-weight: normal; - font-style: italic; - font-display: swap; -} -@font-face { - font-family: 'FiraCode'; - src: url('../fonts/FiraCode-BoldItalic.otf') format('opentype'); - font-weight: bold; - font-style: italic; - font-display: swap; -} /*** Overlock ***/ @font-face { diff --git a/ui/styles/forms.css b/ui/styles/forms.css index 88a6c41..eb98743 100644 --- a/ui/styles/forms.css +++ b/ui/styles/forms.css @@ -5,7 +5,7 @@ label { } label input { - font-family: 'Overlock'; + font-family: 'Overlock', cursive; display: block; width: 90%; padding: 0.25rem; @@ -14,7 +14,7 @@ label input { } form.form > button { - font-family: 'Overlock'; + font-family: 'Overlock', cursive; background-color: var(--colour-input-bg); color: var(--colour-input-text); padding: 0.25rem; diff --git a/ui/styles/messages.css b/ui/styles/messages.css index c4ef106..4890f2c 100644 --- a/ui/styles/messages.css +++ b/ui/styles/messages.css @@ -51,6 +51,7 @@ .message:hover { background-color: var(--colour-message-hover-bg); } + .message:hover * { color: var(--colour-message-hover-text); } @@ -112,11 +113,10 @@ .message-body pre { border: 1px solid #312e81; border-radius: 0.25rem; - background-color: var(--colour-message-run-text); padding: 0.25rem; } .message-body code, .message-body pre { - font-family: 'FiraCode'; + font-family: 'FiraCode', monospace; } diff --git a/ui/styles/reset.css b/ui/styles/reset.css index f9fa505..5a17f02 100644 --- a/ui/styles/reset.css +++ b/ui/styles/reset.css @@ -93,6 +93,7 @@ video { /* font: inherit; */ vertical-align: baseline; } + /* HTML5 display-role reset for older browsers */ article, aside, @@ -107,24 +108,28 @@ nav, section { display: block; } + body { line-height: 1; } + ol, ul { list-style: none; } + blockquote, q { quotes: none; } + blockquote:before, blockquote:after, q:before, q:after { - content: ''; content: none; } + table { border-collapse: collapse; border-spacing: 0; diff --git a/ui/styles/sidebar.css b/ui/styles/sidebar.css index 5e5e16a..c6aab6a 100644 --- a/ui/styles/sidebar.css +++ b/ui/styles/sidebar.css @@ -4,6 +4,8 @@ } .list-nav a { + display: block; + padding: 0.5rem; text-decoration: none; } @@ -12,7 +14,6 @@ } .list-nav li { - padding: 0.5rem; border-radius: 0.5rem; border: 1px solid var(--colour-navbar-border); margin: 0.25rem; diff --git a/ui/styles/textarea.css b/ui/styles/textarea.css index d9be0d6..4b8602c 100644 --- a/ui/styles/textarea.css +++ b/ui/styles/textarea.css @@ -18,7 +18,7 @@ flex-grow: 1; background-color: var(--colour-input-bg); color: var(--colour-input-text); - font-family: 'FiraCode'; + font-family: 'FiraCode', monospace; } .create-message button { |
