feat(blahctl): add database commands to modify users and rooms

This commit is contained in:
oxalica 2024-09-24 22:59:32 -04:00
parent a972477171
commit 066061e2ec
3 changed files with 119 additions and 2 deletions

1
Cargo.lock generated
View file

@ -300,6 +300,7 @@ dependencies = [
"rand",
"reqwest",
"rusqlite",
"serde_jcs",
"serde_json",
"tokio",
]

View file

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

View file

@ -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<IdDescArgs>,
/// User permission.
#[arg(long, value_parser = flag_parser::<ServerPermission>)]
permission: ServerPermission,
},
/// Set property of an existing user.
SetUser {
#[command(flatten)]
user: Box<User>,
/// User permission.
#[arg(long, value_parser = flag_parser::<ServerPermission>)]
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::<RoomAttrs>)]
attrs: Option<RoomAttrs>,
/// 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::<RoomAttrs>)]
attrs: Option<RoomAttrs>,
/// New title.
#[arg(long)]
title: Option<String>,
},
}
fn flag_parser<T: bitflags::Flags>(s: &str) -> clap::error::Result<T> {
@ -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(())
}