Switch room identifier from UUID to stringified i64

This commit is contained in:
oxalica 2024-09-06 00:52:53 -04:00
parent 236fb61832
commit 59d51937da
11 changed files with 381 additions and 143 deletions

248
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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)?;

View file

@ -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"] }

View file

@ -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

View file

@ -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;

View file

@ -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 {

View file

@ -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<RoomMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
skip_token: Option<u64>,
skip_token: Option<Id>,
}
#[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<CreateRoomPayload>,
) -> Result<Json<Uuid>, ApiError> {
) -> Result<Json<Id>, ApiError> {
let members = &params.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<Path<Uuid>>,
R(Path(rid), _): RE<Path<Id>>,
R(Query(pagination), _): RE<Query<Pagination>>,
auth: MaybeAuth,
) -> Result<Json<RoomItems>, 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<Path<Uuid>>,
R(Path(rid), _): RE<Path<Id>>,
auth: MaybeAuth,
) -> Result<Json<RoomMetadata>, 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<Path<Uuid>>,
R(Path(rid), _): RE<Path<Id>>,
R(Query(pagination), _): RE<Query<Pagination>>,
) -> Result<impl IntoResponse, ApiError> {
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<T>(
conn: &rusqlite::Connection,
ruuid: Uuid,
rid: Id,
user: Option<&UserKey>,
f: impl FnOnce(&Row<'_>) -> rusqlite::Result<T>,
) -> Result<T, ApiError> {
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<T>(
`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<ChatItemWithId>, Option<u64>), 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<Path<Uuid>>,
R(Path(rid), _): RE<Path<Id>>,
SignedJson(chat): SignedJson<ChatPayload>,
) -> Result<Json<u64>, 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<Path<Uuid>>,
R(Path(rid), _): RE<Path<Id>>,
SignedJson(op): SignedJson<RoomAdminPayload>,
) -> Result<StatusCode, ApiError> {
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 {

View file

@ -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}`);

View file

@ -1,6 +1,5 @@
// Re-export of public dependencies.
pub use bitflags;
pub use ed25519_dalek;
pub use uuid;
pub mod types;

View file

@ -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<T: Serialize> WithSig<T> {
#[serde(tag = "typ", rename = "chat")]
pub struct ChatPayload {
pub rich_text: RichText,
pub room: Uuid,
pub room: Id,
}
/// Ref: <https://github.com/Blah-IM/Weblah/blob/a3fa0f265af54c846f8d65f42aa4409c8dba9dd9/src/lib/richText.ts>
@ -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<ToSqlOutput<'_>> {
self.0.to_sql()
}
}
impl FromSql for Id {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
i64::column_result(value).map(Self)
}
}
impl ToSql for UserKey {
fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
// 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::<WithSig<ChatPayload>>(&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::<RoomAdminPayload>(&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::<RoomAdminPayload>(&raw).unwrap();
assert_eq!(roundtrip, data);
}
}