From 59d51937daa6cd8de4084564a00052bb94eadf50 Mon Sep 17 00:00:00 2001 From: oxalica Date: Fri, 6 Sep 2024 00:52:53 -0400 Subject: [PATCH] Switch room identifier from UUID to stringified i64 --- Cargo.lock | 248 ++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 5 +- blahctl/src/main.rs | 7 +- blahd/Cargo.toml | 3 +- blahd/docs/webapi.yaml | 22 ++-- blahd/schema.sql | 3 +- blahd/src/database.rs | 2 +- blahd/src/main.rs | 149 ++++++++++++------------- pages/main.js | 26 ++--- src/lib.rs | 1 - src/types.rs | 58 +++++++--- 11 files changed, 381 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b62a7e9..2aae64b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.15" @@ -251,6 +266,7 @@ dependencies = [ "bitflags", "bitflags_serde_shim", "ed25519-dalek", + "expect-test", "hex", "html-escape", "rand", @@ -259,7 +275,7 @@ dependencies = [ "serde", "serde_jcs", "serde_json", - "uuid", + "serde_with", ] [[package]] @@ -304,7 +320,6 @@ dependencies = [ "tracing", "tracing-subscriber", "url", - "uuid", ] [[package]] @@ -349,6 +364,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + [[package]] name = "clap" version = "4.5.16" @@ -463,6 +491,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -480,6 +543,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.7" @@ -490,6 +563,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + [[package]] name = "ed25519" version = "2.2.3" @@ -540,6 +619,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "expect-test" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0be0a561335815e06dab7c62e50353134c796e7a6155402a64bcff66b6a5e0" +dependencies = [ + "dissimilar", + "once_cell", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -686,13 +775,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.4.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -708,7 +803,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -867,6 +962,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -877,6 +1001,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.4.0" @@ -884,7 +1019,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1024,6 +1160,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.3" @@ -1175,6 +1326,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1311,7 +1468,6 @@ dependencies = [ "hashlink", "libsqlite3-sys", "smallvec", - "uuid", ] [[package]] @@ -1526,6 +1682,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.4.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1715,6 +1901,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2005,16 +2222,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" -dependencies = [ - "getrandom", - "serde", -] - [[package]] name = "valuable" version = "0.1.0" @@ -2147,6 +2354,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-registry" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 037ed92..5da8b87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,10 @@ rusqlite = { version = "0.32", optional = true } serde = { version = "1", features = ["derive"] } serde_jcs = "0.1" serde_json = "1" -uuid = { version = "1", features = ["serde"] } +serde_with = "3.9.0" + +[dev-dependencies] +expect-test = "1.5.0" [lints] workspace = true diff --git a/blahctl/src/main.rs b/blahctl/src/main.rs index 3907291..5660afb 100644 --- a/blahctl/src/main.rs +++ b/blahctl/src/main.rs @@ -5,10 +5,9 @@ use std::{fs, io}; use anyhow::{Context, Result}; use blah::bitflags; use blah::types::{ - get_timestamp, ChatPayload, CreateRoomPayload, MemberPermission, RichText, RoomAttrs, + get_timestamp, ChatPayload, CreateRoomPayload, Id, MemberPermission, RichText, RoomAttrs, RoomMember, RoomMemberList, ServerPermission, UserKey, WithSig, }; -use blah::uuid::Uuid; use ed25519_dalek::pkcs8::spki::der::pem::LineEnding; use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey}; use ed25519_dalek::{SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH}; @@ -94,7 +93,7 @@ enum ApiCommand { private_key_file: PathBuf, #[arg(long)] - room: Uuid, + room: i64, #[arg(long)] text: String, @@ -249,7 +248,7 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> { } => { let key = load_signing_key(&private_key_file)?; let payload = ChatPayload { - room, + room: Id(room), rich_text: RichText::from(text), }; let payload = WithSig::sign(&key, get_timestamp(), &mut OsRng, payload)?; diff --git a/blahd/Cargo.toml b/blahd/Cargo.toml index 62ff783..6276a9b 100644 --- a/blahd/Cargo.toml +++ b/blahd/Cargo.toml @@ -14,7 +14,7 @@ futures-util = "0.3" hex = { version = "0.4", features = ["serde"] } humantime = "2" parking_lot = "0.12" # Maybe no better performance, just that we hate poisoning. ¯\_(ツ)_/¯ -rusqlite = { version = "0.32", features = ["uuid"] } +rusqlite = "0.32" sd-notify = "0.4" serde = { version = "1", features = ["derive"] } serde-inline-default = "0.2.0" @@ -26,7 +26,6 @@ tower-http = { version = "0.5", features = ["cors", "limit"] } tracing = "0.1" tracing-subscriber = "0.3" url = { version = "2.5.2", features = ["serde"] } -uuid = { version = "1", features = ["v4"] } blah = { path = "..", features = ["rusqlite"] } diff --git a/blahd/docs/webapi.yaml b/blahd/docs/webapi.yaml index 1329051..81f8383 100644 --- a/blahd/docs/webapi.yaml +++ b/blahd/docs/webapi.yaml @@ -82,14 +82,14 @@ paths: content: application/json: type: string - description: UUID of the newly created room (ruuid). + description: Id of the newly created room (rid). 403: description: The user does not have permission to create room. content: application/json: $ref: '#/components/schemas/ApiError' - /room/{ruuid}: + /room/{rid}: get: summary: Get room metadata responses: @@ -107,9 +107,9 @@ paths: $ref: '#/components/schemas/ApiError' - /room/{ruuid}/feed.json: + /room/{rid}/feed.json: get: - summary: JSON feed of room {ruuid}, which must be public readable + summary: JSON feed of room {rid}, which must be public readable description: For human and feed reader consumption only. responses: 200: @@ -122,9 +122,9 @@ paths: application/json: $ref: '#/components/schemas/ApiError' - /room/{ruuid}/item: + /room/{rid}/item: get: - summary: Get chat history for room {ruuid} + summary: Get chat history for room {rid} description: | Return chat items in reversed time order, up to PAGE_LEN items. The last (oldest) chat id can be used as query parameter for the next @@ -159,7 +159,7 @@ paths: $ref: '#/components/schemas/ApiError' post: - summary: Post a chat in room {ruuid} + summary: Post a chat in room {rid} requestBody: content: application/json: @@ -193,7 +193,7 @@ paths: application/json: $ref: '#/components/schemas/ApiError' - /room/{ruuid}/admin: + /room/{rid}/admin: post: summary: Room management requestBody: @@ -251,9 +251,9 @@ components: RoomMetadataForList: type: object - required: ['ruuid', 'title', 'attrs'] + required: ['rid', 'title', 'attrs'] properties: - ruuid: + rid: type: string title: type: string @@ -265,7 +265,7 @@ components: RoomMetadata: type: object properties: - ruuid: + rid: type: string title: type: string diff --git a/blahd/schema.sql b/blahd/schema.sql index 15c4058..7ebc5cf 100644 --- a/blahd/schema.sql +++ b/blahd/schema.sql @@ -8,8 +8,7 @@ CREATE TABLE IF NOT EXISTS `user` ( ) STRICT; CREATE TABLE IF NOT EXISTS `room` ( - `rid` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - `ruuid` BLOB NOT NULL UNIQUE, + `rid` INTEGER NOT NULL PRIMARY KEY, `title` TEXT NOT NULL, `attrs` INTEGER NOT NULL ) STRICT; diff --git a/blahd/src/database.rs b/blahd/src/database.rs index 2e7ab1d..8c646ae 100644 --- a/blahd/src/database.rs +++ b/blahd/src/database.rs @@ -10,7 +10,7 @@ static INIT_SQL: &str = include_str!("../schema.sql"); // Simple and stupid version check for now. // `echo -n 'blahd-database-0' | sha256sum | head -c5` || version -const APPLICATION_ID: i32 = 0xd9e_8400; +const APPLICATION_ID: i32 = 0xd9e_8401; #[derive(Debug)] pub struct Database { diff --git a/blahd/src/main.rs b/blahd/src/main.rs index f25fd8d..97bc3aa 100644 --- a/blahd/src/main.rs +++ b/blahd/src/main.rs @@ -12,7 +12,7 @@ use axum::routing::{get, post}; use axum::{Json, Router}; use axum_extra::extract::WithRejection as R; use blah::types::{ - ChatItem, ChatPayload, CreateRoomPayload, MemberPermission, RoomAdminOp, RoomAdminPayload, + ChatItem, ChatPayload, CreateRoomPayload, Id, MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission, Signee, UserKey, WithSig, }; use config::Config; @@ -24,7 +24,6 @@ use rusqlite::{named_params, params, Connection, OptionalExtension, Row, ToSql}; use serde::{Deserialize, Serialize}; use url::Url; use utils::ExpiringSet; -use uuid::Uuid; #[macro_use] mod middleware; @@ -143,11 +142,11 @@ async fn main_async(st: AppState) -> Result<()> { .route("/ws", get(handle_ws)) .route("/room", get(room_list)) .route("/room/create", post(room_create)) - .route("/room/:ruuid", get(room_get_metadata)) + .route("/room/:rid", get(room_get_metadata)) // NB. Sync with `feed_url` and `next_url` generation. - .route("/room/:ruuid/feed.json", get(room_get_feed)) - .route("/room/:ruuid/item", get(room_get_item).post(room_post_item)) - .route("/room/:ruuid/admin", post(room_admin)) + .route("/room/:rid/feed.json", get(room_get_feed)) + .route("/room/:rid/item", get(room_get_item).post(room_post_item)) + .route("/room/:rid/admin", post(room_admin)) .with_state(st.clone()) .layer(tower_http::limit::RequestBodyLimitLayer::new( st.config.server.max_request_len, @@ -195,7 +194,7 @@ async fn handle_ws(State(st): ArcState, ws: WebSocketUpgrade) -> Response { struct RoomList { rooms: Vec, #[serde(skip_serializing_if = "Option::is_none")] - skip_token: Option, + skip_token: Option, } #[derive(Debug, Deserialize)] @@ -235,8 +234,8 @@ async fn room_list( .prepare(sql)? .query_map(params, |row| { // TODO: Extract this into a function. - last_rid = Some(row.get::<_, u64>("rid")?); - let ruuid = row.get("ruuid")?; + let rid = row.get("rid")?; + last_rid = Some(rid); let title = row.get("title")?; let attrs = row.get("attrs")?; let last_chat = row @@ -250,14 +249,14 @@ async fn room_list( user, payload: ChatPayload { rich_text: row.get("rich_text")?, - room: ruuid, + room: rid, }, }, }) }) .transpose()?; Ok(RoomMetadata { - ruuid, + rid, title, attrs, last_chat, @@ -271,7 +270,7 @@ async fn room_list( match params.filter { ListRoomFilter::Public => query( r" - SELECT `rid`, `ruuid`, `title`, `attrs`, + SELECT `rid`, `title`, `attrs`, `last_author`.`userkey` AS `userkey`, `timestamp`, `nonce`, `sig`, `rich_text` FROM `room` LEFT JOIN `room_item` USING (`rid`) @@ -293,7 +292,7 @@ async fn room_list( query( r" SELECT - `rid`, `ruuid`, `title`, `attrs`, + `rid`, `title`, `attrs`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text` FROM `user` JOIN `room_member` USING (`uid`) @@ -320,7 +319,7 @@ async fn room_list( async fn room_create( st: ArcState, SignedJson(params): SignedJson, -) -> Result, ApiError> { +) -> Result, ApiError> { let members = ¶ms.signee.payload.members.0; if !members .iter() @@ -356,21 +355,18 @@ async fn room_create( )); }; - let ruuid = Uuid::new_v4(); - let txn = conn.transaction()?; txn.execute( r" - INSERT INTO `room` (`ruuid`, `title`, `attrs`) - VALUES (:ruuid, :title, :attrs) + INSERT INTO `room` (`title`, `attrs`) + VALUES (:title, :attrs) ", named_params! { - ":ruuid": ruuid, ":title": params.signee.payload.title, ":attrs": params.signee.payload.attrs, }, )?; - let rid = txn.last_insert_rowid() as u64; + let rid = Id(txn.last_insert_rowid()); let mut insert_user = txn.prepare( r" INSERT INTO `user` (`userkey`) @@ -398,7 +394,7 @@ async fn room_create( drop(insert_user); txn.commit()?; - Ok(Json(ruuid)) + Ok(Json(rid)) } /// Pagination query parameters. @@ -432,14 +428,14 @@ struct RoomItems { async fn room_get_item( st: ArcState, - R(Path(ruuid), _): RE>, + R(Path(rid), _): RE>, R(Query(pagination), _): RE>, auth: MaybeAuth, ) -> Result, ApiError> { let (items, skip_token) = { let conn = st.db.get(); - get_room_if_readable(&conn, ruuid, auth.into_optional()?.as_ref(), |_row| Ok(()))?; - query_room_items(&st, &conn, ruuid, pagination)? + get_room_if_readable(&conn, rid, auth.into_optional()?.as_ref(), |_row| Ok(()))?; + query_room_items(&st, &conn, rid, pagination)? }; let items = items.into_iter().map(|(_, item)| item).collect(); Ok(Json(RoomItems { @@ -450,11 +446,11 @@ async fn room_get_item( async fn room_get_metadata( st: ArcState, - R(Path(ruuid), _): RE>, + R(Path(rid), _): RE>, auth: MaybeAuth, ) -> Result, ApiError> { let (title, attrs) = - get_room_if_readable(&st.db.get(), ruuid, auth.into_optional()?.as_ref(), |row| { + get_room_if_readable(&st.db.get(), rid, auth.into_optional()?.as_ref(), |row| { Ok(( row.get::<_, String>("title")?, row.get::<_, RoomAttrs>("attrs")?, @@ -462,7 +458,7 @@ async fn room_get_metadata( })?; Ok(Json(RoomMetadata { - ruuid, + rid, title, attrs, last_chat: None, @@ -471,14 +467,14 @@ async fn room_get_metadata( async fn room_get_feed( st: ArcState, - R(Path(ruuid), _): RE>, + R(Path(rid), _): RE>, R(Query(pagination), _): RE>, ) -> Result { let title; let (items, skip_token) = { let conn = st.db.get(); - title = get_room_if_readable(&conn, ruuid, None, |row| row.get::<_, String>("title"))?; - query_room_items(&st, &conn, ruuid, pagination)? + title = get_room_if_readable(&conn, rid, None, |row| row.get::<_, String>("title"))?; + query_room_items(&st, &conn, rid, pagination)? }; let items = items @@ -506,7 +502,7 @@ async fn room_get_feed( .config .server .base_url - .join(&format!("/room/{ruuid}/feed.json")) + .join(&format!("/room/{rid}/feed.json")) .expect("base_url must be valid"); let next_url = skip_token.map(|skip_token| { let next_params = Pagination { @@ -573,7 +569,7 @@ struct FeedItemExtra { #[derive(Debug, Serialize)] pub struct RoomMetadata { - pub ruuid: Uuid, + pub rid: Id, pub title: String, pub attrs: RoomAttrs, @@ -584,15 +580,15 @@ pub struct RoomMetadata { fn get_room_if_readable( conn: &rusqlite::Connection, - ruuid: Uuid, + rid: Id, user: Option<&UserKey>, f: impl FnOnce(&Row<'_>) -> rusqlite::Result, ) -> Result { conn.query_row( r" - SELECT `rid`, `title`, `attrs` + SELECT `title`, `attrs` FROM `room` - WHERE `ruuid` = :ruuid AND + WHERE `rid` = :rid AND ((`attrs` & :perm) = :perm OR EXISTS(SELECT 1 FROM `room_member` @@ -601,8 +597,8 @@ fn get_room_if_readable( `userkey` = :userkey)) ", named_params! { + ":rid": rid, ":perm": RoomAttrs::PUBLIC_READABLE, - ":ruuid": ruuid, ":userkey": user, }, f, @@ -618,17 +614,16 @@ type ChatItemWithId = (u64, ChatItem); fn query_room_items( st: &AppState, conn: &Connection, - ruuid: Uuid, + rid: Id, pagination: Pagination, ) -> Result<(Vec, Option), ApiError> { let page_len = pagination.effective_page_len(st); let mut stmt = conn.prepare( r" SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text` - FROM `room` - JOIN `room_item` USING (`rid`) + FROM `room_item` JOIN `user` USING (`uid`) - WHERE `ruuid` = :ruuid AND + WHERE `rid` = :rid AND (:before_cid IS NULL OR `cid` < :before_cid) ORDER BY `cid` DESC LIMIT :limit @@ -637,7 +632,7 @@ fn query_room_items( let items = stmt .query_and_then( named_params! { - ":ruuid": ruuid, + ":rid": rid, ":before_cid": pagination.skip_token, ":limit": page_len, }, @@ -650,7 +645,7 @@ fn query_room_items( timestamp: row.get("timestamp")?, user: row.get("userkey")?, payload: ChatPayload { - room: ruuid, + room: rid, rich_text: row.get("rich_text")?, }, }, @@ -667,10 +662,10 @@ fn query_room_items( async fn room_post_item( st: ArcState, - R(Path(ruuid), _): RE>, + R(Path(rid), _): RE>, SignedJson(chat): SignedJson, ) -> Result, ApiError> { - if ruuid != chat.signee.payload.room { + if rid != chat.signee.payload.room { return Err(error_response!( StatusCode::BAD_REQUEST, "invalid_request", @@ -680,25 +675,28 @@ async fn room_post_item( let (cid, txs) = { let conn = st.db.get(); - let Some((rid, uid)) = conn + let Some((uid, _perm)) = conn .query_row( r" - SELECT `rid`, `uid` - FROM `room` - JOIN `room_member` USING (`rid`) + SELECT `uid`, `room_member`.`permission` + FROM `room_member` JOIN `user` USING (`uid`) - WHERE `ruuid` = :ruuid AND - `userkey` = :userkey AND - (`room_member`.`permission` & :perm) = :perm + WHERE `rid` = :rid AND + `userkey` = :userkey ", named_params! { - ":ruuid": ruuid, + ":rid": rid, ":userkey": &chat.signee.user, - ":perm": MemberPermission::POST_CHAT, }, - |row| Ok((row.get::<_, u64>("rid")?, row.get::<_, u64>("uid")?)), + |row| { + Ok(( + row.get::<_, u64>("uid")?, + row.get::<_, MemberPermission>("permission")?, + )) + }, ) .optional()? + .filter(|(_, perm)| perm.contains(MemberPermission::POST_CHAT)) else { return Err(error_response!( StatusCode::FORBIDDEN, @@ -759,10 +757,10 @@ async fn room_post_item( async fn room_admin( st: ArcState, - R(Path(ruuid), _): RE>, + R(Path(rid), _): RE>, SignedJson(op): SignedJson, ) -> Result { - if ruuid != op.signee.payload.room { + if rid != op.signee.payload.room { return Err(error_response!( StatusCode::BAD_REQUEST, "invalid_request", @@ -786,7 +784,7 @@ async fn room_admin( "invalid permission", )); } - room_join(&st, ruuid, user, permission).await?; + room_join(&st, rid, user, permission).await?; } RoomAdminOp::RemoveMember { user } => { if user != op.signee.user { @@ -796,7 +794,7 @@ async fn room_admin( "only self-removing is implemented yet", )); } - room_leave(&st, ruuid, user).await?; + room_leave(&st, rid, user).await?; } } @@ -805,34 +803,32 @@ async fn room_admin( async fn room_join( st: &AppState, - ruuid: Uuid, + rid: Id, user: UserKey, permission: MemberPermission, ) -> Result<(), ApiError> { let mut conn = st.db.get(); let txn = conn.transaction()?; - let Some(rid) = txn + let is_public_joinable = txn .query_row( r" - SELECT `rid` + SELECT `attrs` FROM `room` - WHERE `ruuid` = :ruuid AND - (`room`.`attrs` & :joinable) = :joinable + WHERE `rid` = ? ", - named_params! { - ":ruuid": ruuid, - ":joinable": RoomAttrs::PUBLIC_JOINABLE, - }, - |row| row.get::<_, u64>("rid"), + params![rid], + |row| row.get::<_, RoomAttrs>(0), ) .optional()? - else { + .is_some_and(|attrs| attrs.contains(RoomAttrs::PUBLIC_JOINABLE)); + if !is_public_joinable { return Err(error_response!( StatusCode::FORBIDDEN, "permission_denied", "room does not exists or user is not allowed to join this room", )); - }; + } + txn.execute( r" INSERT INTO `user` (`userkey`) @@ -860,25 +856,24 @@ async fn room_join( Ok(()) } -async fn room_leave(st: &AppState, ruuid: Uuid, user: UserKey) -> Result<(), ApiError> { +async fn room_leave(st: &AppState, rid: Id, user: UserKey) -> Result<(), ApiError> { let mut conn = st.db.get(); let txn = conn.transaction()?; - let Some((rid, uid)) = txn + let Some(uid) = txn .query_row( r" - SELECT `rid`, `uid` + SELECT `uid` FROM `room_member` - JOIN `room` USING (`rid`) JOIN `user` USING (`uid`) - WHERE `ruuid` = :ruuid AND + WHERE `rid` = :rid AND `userkey` = :userkey ", named_params! { - ":ruuid": ruuid, + ":rid": rid, ":userkey": user, }, - |row| Ok((row.get::<_, u64>("rid")?, row.get::<_, u64>("uid")?)), + |row| row.get::<_, u64>("uid"), ) .optional()? else { diff --git a/pages/main.js b/pages/main.js index ef9479c..3e93caf 100644 --- a/pages/main.js +++ b/pages/main.js @@ -168,13 +168,13 @@ async function genAuthHeader() { }; } -async function enterRoom(ruuid) { - log(`loading room: ${ruuid}`); - curRoom = ruuid; - roomsInput.value = ruuid; +async function enterRoom(rid) { + log(`loading room: ${rid}`); + curRoom = rid; + roomsInput.value = rid; genAuthHeader() - .then(opts => fetch(`${serverUrl}/room/${ruuid}`, opts)) + .then(opts => fetch(`${serverUrl}/room/${rid}`, opts)) .then(async (resp) => [resp.status, await resp.json()]) .then(async ([status, json]) => { if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); @@ -185,7 +185,7 @@ async function enterRoom(ruuid) { }); genAuthHeader() - .then(opts => fetch(`${serverUrl}/room/${ruuid}/item`, opts)) + .then(opts => fetch(`${serverUrl}/room/${rid}/item`, opts)) .then(async (resp) => { return [resp.status, await resp.json()]; }) .then(async ([status, json]) => { if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); @@ -267,10 +267,10 @@ async function loadRoomList(autoJoin) { const resp = await fetch(`${serverUrl}/room?filter=${filter}`, await genAuthHeader()) const json = await resp.json() if (resp.status !== 200) throw new Error(`status ${resp.status}: ${json.error.message}`); - for (const { ruuid, title, attrs } of json.rooms) { + for (const { rid, title, attrs } of json.rooms) { const el = document.createElement('option'); - el.value = ruuid; - el.innerText = `${title} (uuid=${ruuid}, attrs=${attrs})`; + el.value = rid; + el.innerText = `${title} (rid=${rid}, attrs=${attrs})`; targetEl.appendChild(el); } } catch (err) { @@ -291,19 +291,19 @@ async function loadRoomList(autoJoin) { loadInto(joinNewRoomInput, 'public') } -async function joinRoom(ruuid) { +async function joinRoom(rid) { try { joinNewRoomInput.disabled = true; - await signAndPost(`${serverUrl}/room/${ruuid}/admin`, { + await signAndPost(`${serverUrl}/room/${rid}/admin`, { // sorted fields. permission: 1, // POST_CHAT - room: ruuid, + room: rid, typ: 'add_member', user: await getUserPubkey(), }); log('joined room'); await loadRoomList(false) - await enterRoom(ruuid); + await enterRoom(rid); } catch (e) { console.error(e); log(`failed to join room: ${e}`); diff --git a/src/lib.rs b/src/lib.rs index 308ea54..cc9a52c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ // Re-export of public dependencies. pub use bitflags; pub use ed25519_dalek; -pub use uuid; pub mod types; diff --git a/src/types.rs b/src/types.rs index 9a4222d..2954ce7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,7 +9,20 @@ use ed25519_dalek::{ }; use rand_core::RngCore; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -use uuid::Uuid; +use serde_with::{serde_as, DisplayFromStr}; + +/// An opaque server-specific ID for room, chat item, and etc. +/// It's currently serialized as a string for JavaScript's convenience. +#[serde_as] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Id(#[serde_as(as = "DisplayFromStr")] pub i64); + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] @@ -82,7 +95,7 @@ impl WithSig { #[serde(tag = "typ", rename = "chat")] pub struct ChatPayload { pub rich_text: RichText, - pub room: Uuid, + pub room: Id, } /// Ref: @@ -329,7 +342,7 @@ pub struct AuthPayload {} #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "typ", rename_all = "snake_case")] pub struct RoomAdminPayload { - pub room: Uuid, + pub room: Id, #[serde(flatten)] pub op: RoomAdminOp, } @@ -385,6 +398,18 @@ mod sql_impl { use super::*; + impl ToSql for Id { + fn to_sql(&self) -> Result> { + self.0.to_sql() + } + } + + impl FromSql for Id { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + i64::column_result(value).map(Self) + } + } + impl ToSql for UserKey { fn to_sql(&self) -> Result> { // TODO: Extensive key format? @@ -441,6 +466,8 @@ mod sql_impl { #[cfg(test)] mod tests { + use expect_test::expect; + use super::*; #[test] @@ -454,16 +481,17 @@ mod tests { &mut fake_rng, ChatPayload { rich_text: RichText::from("hello"), - room: Uuid::nil(), + room: Id(42), }, ) .unwrap(); let json = serde_jcs::to_string(&item).unwrap(); - assert_eq!( - json, - r#"{"sig":"5e52985dc9e43a77267f0b383a8223af96f36e83c180a36da627dfac6504b2bb4c6b80c9903a6c3a0bbc742718466d72af4407a8e74d41af5cb0137cf3798d08","signee":{"nonce":66,"payload":{"rich_text":["hello"],"room":"00000000-0000-0000-0000-000000000000","typ":"chat"},"timestamp":3735928559,"user":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"}}"# - ); + let expect = expect![[ + r#"{"sig":"18ee190722bebfd438c82f34890540d91578b4ba9f6c0c6011cc4fd751a321e32e9442d00dad1920799c54db011694c72a9ba993b408922e9997119209aa5e09","signee":{"nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559,"user":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"}}"# + ]]; + expect.assert_eq(&json); + let roundtrip_item = serde_json::from_str::>(&json).unwrap(); // assert_eq!(roundtrip_item, item); roundtrip_item.verify().unwrap(); @@ -503,7 +531,7 @@ mod tests { #[test] fn room_admin_serde() { let data = RoomAdminPayload { - room: Uuid::nil(), + room: Id(42), op: RoomAdminOp::AddMember { permission: MemberPermission::POST_CHAT, user: UserKey([0x42; PUBLIC_KEY_LENGTH]), @@ -511,11 +539,11 @@ mod tests { }; let raw = serde_jcs::to_string(&data).unwrap(); - assert_eq!( - raw, - r#"{"permission":1,"room":"00000000-0000-0000-0000-000000000000","typ":"add_member","user":"4242424242424242424242424242424242424242424242424242424242424242"}"# - ); - let got = serde_json::from_str::(&raw).unwrap(); - assert_eq!(got, data); + let expect = expect![[ + r#"{"permission":1,"room":"42","typ":"add_member","user":"4242424242424242424242424242424242424242424242424242424242424242"}"# + ]]; + expect.assert_eq(&raw); + let roundtrip = serde_json::from_str::(&raw).unwrap(); + assert_eq!(roundtrip, data); } }