diff --git a/Cargo.lock b/Cargo.lock index 12132bb..589311f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,7 @@ dependencies = [ "rand", "reqwest", "rusqlite", + "serde_jcs", "serde_json", "tokio", ] diff --git a/blahctl/Cargo.toml b/blahctl/Cargo.toml index 5ec41e9..32489cc 100644 --- a/blahctl/Cargo.toml +++ b/blahctl/Cargo.toml @@ -12,6 +12,7 @@ humantime = "2" rand = "0.8" reqwest = { version = "0.12", features = ["json"] } rusqlite = { version = "0.32", features = ["rusqlite-macros"] } +serde_jcs = "0.1.0" serde_json = "1" tokio = { version = "1", features = ["rt", "macros"] } diff --git a/blahctl/src/main.rs b/blahctl/src/main.rs index cc17bce..db6f4b7 100644 --- a/blahctl/src/main.rs +++ b/blahctl/src/main.rs @@ -5,13 +5,14 @@ use std::time::SystemTime; use anyhow::{ensure, Context, Result}; use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile}; use blah_types::{bitflags, get_timestamp, PubKey, RoomAttrs, ServerPermission, SignExt}; +use clap::value_parser; use ed25519_dalek::pkcs8::spki::der::pem::LineEnding; use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey}; use ed25519_dalek::{SigningKey, VerifyingKey}; use humantime::Duration; use rand::thread_rng; use reqwest::Url; -use rusqlite::{prepare_and_bind, Connection}; +use rusqlite::{named_params, prepare_and_bind, Connection}; use tokio::runtime::Runtime; const USER_AGENT: &str = concat!("blahctl/", env!("CARGO_PKG_VERSION")); @@ -182,14 +183,52 @@ impl IdDescArgs { enum DbCommand { /// Create and initialize database. Init, + /// Add a new user or update identity and act_keys of an existing user. + RegisterUser { + #[command(flatten)] + user: Box, + + /// User permission. + #[arg(long, value_parser = flag_parser::)] + permission: ServerPermission, + }, /// Set property of an existing user. SetUser { #[command(flatten)] user: Box, + /// User permission. #[arg(long, value_parser = flag_parser::)] permission: ServerPermission, }, + /// Create an empty room. + CreateRoom { + /// Room id. + #[arg(long, value_parser = value_parser!(i64).range(0..))] + rid: i64, + + /// Room attributes. + #[arg(long, value_parser = flag_parser::)] + attrs: Option, + + /// Room title. + #[arg(long)] + title: String, + }, + /// Update attributes of an existing room. + SetRoom { + /// Room id. + #[arg(long, value_parser = value_parser!(i64).range(0..))] + rid: i64, + + /// New attributes. + #[arg(long, value_parser = flag_parser::)] + attrs: Option, + + /// New title. + #[arg(long)] + title: Option, + }, } fn flag_parser(s: &str) -> clap::error::Result { @@ -426,9 +465,59 @@ fn main_id(cmd: IdCommand) -> Result<()> { Ok(()) } -fn main_db(conn: Connection, command: DbCommand) -> Result<()> { +fn main_db(mut conn: Connection, command: DbCommand) -> Result<()> { match command { DbCommand::Init => {} + DbCommand::RegisterUser { user, permission } => { + let id_desc = user.load(&build_rt()?)?; + let fetch_time = get_timestamp(); + id_desc + .verify(user.id_url.as_ref(), fetch_time) + .context("invalid identity description")?; + let id_desc_json = serde_jcs::to_string(&id_desc).expect("serialization cannot fail"); + let id_key = &id_desc.id_key; + let txn = conn.transaction()?; + // TODO: These SQLs (partially?) duplicate with `blahd::database::Database`. + let uid = prepare_and_bind!( + txn, + r" + INSERT INTO `user` (`id_key`, `last_fetch_time`, `id_desc`) + VALUES (:id_key, :fetch_time, :id_desc_json) + ON CONFLICT (`id_key`) DO UPDATE SET + `last_fetch_time` = excluded.`last_fetch_time`, + `id_desc` = excluded.`id_desc`, + `permission` = :permission + RETURNING `uid` + " + ) + .raw_query() + .next()? + .expect("should insert or fail") + .get::<_, i64>(0)?; + prepare_and_bind!( + txn, + r" + DELETE FROM `user_act_key` + WHERE `uid` = :uid + " + ) + .raw_execute()?; + let mut stmt = txn.prepare( + r" + INSERT INTO `user_act_key` (`uid`, `act_key`, `expire_time`) + VALUES (:uid, :act_key, :expire_time) + ", + )?; + for kdesc in &id_desc.act_keys { + stmt.execute(named_params! { + ":uid": uid, + ":act_key": kdesc.signee.payload.act_key, + ":expire_time": i64::try_from(kdesc.signee.payload.expire_time).expect("verified timestamp"), + })?; + } + stmt.finalize()?; + txn.commit()?; + } DbCommand::SetUser { user, permission } => { let rt = build_rt()?; let id_key = user.load(&rt)?; @@ -442,6 +531,32 @@ fn main_db(conn: Connection, command: DbCommand) -> Result<()> { ) .raw_execute()?; } + DbCommand::CreateRoom { rid, attrs, title } => { + assert!(rid >= 0, "checked by clap"); + let attrs = attrs.unwrap_or_default(); + prepare_and_bind!( + conn, + r" + INSERT INTO `room` (`rid`, `attrs`, `title`) + VALUES (:rid, :attrs, :title) + " + ) + .raw_execute()?; + } + DbCommand::SetRoom { rid, attrs, title } => { + assert!(rid >= 0, "checked by clap"); + let updated = prepare_and_bind!( + conn, + r" + UPDATE `room` SET + `attrs` = COALESCE(:attrs, `attrs`), + `title` = COALESCE(:title, `title`) + WHERE `rid` = :rid + " + ) + .raw_execute()?; + ensure!(updated == 1, "room does not exist"); + } } Ok(()) }