feat(sdk): wire device_id through messaging and client APIs
Add device_id parameter to fetch, fetch_wait, ack, receive_messages, and receive_messages_wait SDK functions. QpqClient gains device_id field with register_device/list_devices/revoke_device convenience methods. Client REPL passes empty device_id for backwards compat.
This commit is contained in:
@@ -88,7 +88,8 @@ const COMMANDS: &[CmdDef] = &[
|
||||
CmdDef { name: "/history", aliases: &[], category: Category::Messaging, description: "Show recent message history", usage: "/history [count]" },
|
||||
CmdDef { name: "/list", aliases: &["/ls"], category: Category::Messaging, description: "List all conversations", usage: "/list" },
|
||||
CmdDef { name: "/switch", aliases: &["/sw"], category: Category::Messaging, description: "Switch active conversation", usage: "/switch <name>" },
|
||||
CmdDef { name: "/group", aliases: &["/g"], category: Category::Groups, description: "create | invite | leave | list | members", usage: "/group <sub> [args]" },
|
||||
CmdDef { name: "/group", aliases: &["/g"], category: Category::Groups, description: "create | invite | leave | list | members | remove | rename | rotate-keys", usage: "/group <sub> [args]" },
|
||||
CmdDef { name: "/devices", aliases: &[], category: Category::Account, description: "list | add | remove — manage linked devices", usage: "/devices <sub> [args]" },
|
||||
CmdDef { name: "/register", aliases: &[], category: Category::Account, description: "Register a new account", usage: "/register <user> <pass>" },
|
||||
CmdDef { name: "/login", aliases: &[], category: Category::Account, description: "Log in to an existing account", usage: "/login <user> <pass>" },
|
||||
CmdDef { name: "/logout", aliases: &[], category: Category::Account, description: "Log out (clear session)", usage: "/logout" },
|
||||
@@ -161,7 +162,7 @@ impl QpqCompleter {
|
||||
names.push(a.to_string());
|
||||
}
|
||||
}
|
||||
for sub in &["create", "invite", "leave", "list", "members"] {
|
||||
for sub in &["create", "invite", "leave", "list", "members", "remove", "rename", "rotate-keys"] {
|
||||
names.push(format!("/group {sub}"));
|
||||
}
|
||||
Self { names }
|
||||
@@ -383,6 +384,7 @@ async fn dispatch(
|
||||
"/list" | "/ls" => do_list(client)?,
|
||||
"/switch" | "/sw" => do_switch(client, st, args)?,
|
||||
"/group" | "/g" => do_group(client, st, args).await?,
|
||||
"/devices" => do_devices(client, args).await?,
|
||||
_ => display::print_error(&format!("unknown command: {cmd} (try /help)")),
|
||||
}
|
||||
Ok(false)
|
||||
@@ -646,7 +648,7 @@ async fn do_recv(client: &QpqClient, st: &ReplState) -> anyhow::Result<()> {
|
||||
let my_pub = identity.public_key_bytes();
|
||||
|
||||
let messages = quicproquo_sdk::messaging::receive_messages(
|
||||
rpc, &mut member, &my_pub, None, conv_id.0.as_slice(),
|
||||
rpc, &mut member, &my_pub, None, conv_id.0.as_slice(), &[],
|
||||
).await?;
|
||||
|
||||
if messages.is_empty() {
|
||||
@@ -804,7 +806,16 @@ async fn do_group(client: &mut QpqClient, st: &mut ReplState, args: &str) -> any
|
||||
}
|
||||
|
||||
"leave" => {
|
||||
display::print_status("group leave not yet implemented in SDK");
|
||||
let identity = st.require_identity()?;
|
||||
let conv_id = st.require_conversation()?.clone();
|
||||
let rpc = client.rpc().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let conv_store = client.conversations().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let conv = conv_store
|
||||
.load_conversation(&conv_id)?
|
||||
.ok_or_else(|| anyhow::anyhow!("conversation not found"))?;
|
||||
let mut member = quicproquo_sdk::groups::restore_mls_state(&conv, &identity)?;
|
||||
quicproquo_sdk::groups::leave_group(rpc, conv_store, &mut member, &conv_id).await?;
|
||||
display::print_status("left group");
|
||||
}
|
||||
|
||||
"list" => do_list(client)?,
|
||||
@@ -830,7 +841,132 @@ async fn do_group(client: &mut QpqClient, st: &mut ReplState, args: &str) -> any
|
||||
println!();
|
||||
}
|
||||
|
||||
_ => display::print_error("usage: /group <create|invite|leave|list|members> [args]"),
|
||||
"remove" => {
|
||||
let user = parts.get(1).copied().unwrap_or("").trim();
|
||||
if user.is_empty() {
|
||||
display::print_error("usage: /group remove <username>");
|
||||
return Ok(());
|
||||
}
|
||||
let identity = st.require_identity()?;
|
||||
let conv_id = st.require_conversation()?.clone();
|
||||
let rpc = client.rpc().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let conv_store = client.conversations().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
let peer_key = quicproquo_sdk::users::resolve_user(rpc, user)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("user '{user}' not found"))?;
|
||||
let conv = conv_store
|
||||
.load_conversation(&conv_id)?
|
||||
.ok_or_else(|| anyhow::anyhow!("conversation not found"))?;
|
||||
let mut member = quicproquo_sdk::groups::restore_mls_state(&conv, &identity)?;
|
||||
quicproquo_sdk::groups::remove_member_from_group(
|
||||
rpc, conv_store, &mut member, &conv_id, &peer_key,
|
||||
).await?;
|
||||
display::print_status(&format!("removed @{user} from group"));
|
||||
}
|
||||
|
||||
"rename" => {
|
||||
let new_name = parts.get(1).copied().unwrap_or("").trim();
|
||||
if new_name.is_empty() {
|
||||
display::print_error("usage: /group rename <new-name>");
|
||||
return Ok(());
|
||||
}
|
||||
let conv_id = st.require_conversation()?.clone();
|
||||
let rpc = client.rpc().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let conv_store = client.conversations().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
quicproquo_sdk::groups::set_group_metadata(
|
||||
rpc, conv_store, &conv_id, new_name, "", &[],
|
||||
).await?;
|
||||
st.set_conversation(conv_id, format!("#{new_name}"));
|
||||
display::print_status(&format!("group renamed to #{new_name}"));
|
||||
}
|
||||
|
||||
"rotate-keys" => {
|
||||
let identity = st.require_identity()?;
|
||||
let conv_id = st.require_conversation()?.clone();
|
||||
let rpc = client.rpc().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let conv_store = client.conversations().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let conv = conv_store
|
||||
.load_conversation(&conv_id)?
|
||||
.ok_or_else(|| anyhow::anyhow!("conversation not found"))?;
|
||||
let mut member = quicproquo_sdk::groups::restore_mls_state(&conv, &identity)?;
|
||||
quicproquo_sdk::groups::rotate_group_keys(rpc, conv_store, &mut member, &conv_id).await?;
|
||||
display::print_status("group keys rotated");
|
||||
}
|
||||
|
||||
_ => display::print_error("usage: /group <create|invite|leave|list|members|remove|rename|rotate-keys> [args]"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Device management ──────────────────────────────────────────────────────
|
||||
|
||||
async fn do_devices(client: &mut QpqClient, args: &str) -> anyhow::Result<()> {
|
||||
let parts: Vec<&str> = args.splitn(3, char::is_whitespace).collect();
|
||||
let sub = parts.first().copied().unwrap_or("");
|
||||
|
||||
match sub {
|
||||
"list" => {
|
||||
let rpc = client.rpc().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let devices = quicproquo_sdk::devices::list_devices(rpc).await?;
|
||||
if devices.is_empty() {
|
||||
display::print_status("no devices registered");
|
||||
} else {
|
||||
println!("\n{BOLD}Devices{RESET} ({})", devices.len());
|
||||
println!("{:<36} {:<20} REGISTERED AT", "DEVICE ID", "NAME");
|
||||
for d in &devices {
|
||||
println!(
|
||||
"{:<36} {:<20} {}",
|
||||
hex::encode(&d.device_id),
|
||||
d.device_name,
|
||||
d.registered_at,
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
"add" => {
|
||||
let name = parts.get(1).copied().unwrap_or("").trim();
|
||||
if name.is_empty() {
|
||||
display::print_error("usage: /devices add <name>");
|
||||
return Ok(());
|
||||
}
|
||||
let rpc = client.rpc().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
// Generate a random device ID (16 bytes).
|
||||
let mut dev_id = vec![0u8; 16];
|
||||
quicproquo_core::getrandom::fill(&mut dev_id)
|
||||
.map_err(|e| anyhow::anyhow!("rng: {e}"))?;
|
||||
let was_new =
|
||||
quicproquo_sdk::devices::register_device(rpc, &dev_id, name).await?;
|
||||
if was_new {
|
||||
display::print_status(&format!(
|
||||
"device registered: {name} (id: {})",
|
||||
hex::encode(&dev_id)
|
||||
));
|
||||
} else {
|
||||
display::print_status(&format!("device already exists: {name}"));
|
||||
}
|
||||
}
|
||||
|
||||
"remove" => {
|
||||
let id_hex = parts.get(1).copied().unwrap_or("").trim();
|
||||
if id_hex.is_empty() {
|
||||
display::print_error("usage: /devices remove <device-id-hex>");
|
||||
return Ok(());
|
||||
}
|
||||
let id_bytes = hex::decode(id_hex)
|
||||
.map_err(|e| anyhow::anyhow!("invalid device_id hex: {e}"))?;
|
||||
let rpc = client.rpc().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let revoked = quicproquo_sdk::devices::revoke_device(rpc, &id_bytes).await?;
|
||||
if revoked {
|
||||
display::print_status(&format!("device revoked: {id_hex}"));
|
||||
} else {
|
||||
display::print_error(&format!("device not found: {id_hex}"));
|
||||
}
|
||||
}
|
||||
|
||||
_ => display::print_error("usage: /devices <list|add|remove> [args]"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user