feat: DM epoch fix, federation relay, and mDNS mesh discovery
- schema: createChannel returns wasNew :Bool to elect the MLS initiator unambiguously; prevents duplicate group creation on concurrent /dm calls - core: group helpers for epoch tracking and key-package lifecycle - server: federation subsystem — mTLS QUIC server-to-server relay with Cap'n Proto RPC; enqueue/batchEnqueue relay unknown recipients to their home domain via FederationClient - server: mDNS _quicproquo._udp.local. service announcement on startup - server: storage + sql_store — identity_exists, peek/ack, federation home-server lookup helpers - client: /mesh peers REPL command (mDNS discovery, feature = "mesh") - client: MeshDiscovery — background mDNS browse with ServiceDaemon - client: was_new=false path in cmd_dm waits for peer Welcome instead of creating a duplicate initiator group - p2p: fix ALPN from quicnprotochat/p2p/1 → quicproquo/p2p/1 - workspace: re-include quicproquo-p2p in members
This commit is contained in:
@@ -54,6 +54,9 @@ enum SlashCommand {
|
||||
Join,
|
||||
Members,
|
||||
History { count: usize },
|
||||
/// Mesh subcommands: /mesh peers, /mesh server <addr>
|
||||
MeshPeers,
|
||||
MeshServer { addr: String },
|
||||
}
|
||||
|
||||
fn parse_input(line: &str) -> Input {
|
||||
@@ -116,6 +119,22 @@ fn parse_input(line: &str) -> Input {
|
||||
let count = arg.and_then(|s| s.parse().ok()).unwrap_or(20);
|
||||
Input::Slash(SlashCommand::History { count })
|
||||
}
|
||||
"/mesh" => match arg.as_deref() {
|
||||
Some("peers") => Input::Slash(SlashCommand::MeshPeers),
|
||||
Some(rest) if rest.starts_with("server ") => {
|
||||
let addr = rest.trim_start_matches("server ").trim().to_string();
|
||||
if addr.is_empty() {
|
||||
display::print_error("usage: /mesh server <host:port>");
|
||||
Input::Empty
|
||||
} else {
|
||||
Input::Slash(SlashCommand::MeshServer { addr })
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
display::print_error("usage: /mesh peers | /mesh server <host:port>");
|
||||
Input::Empty
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
display::print_error(&format!("unknown command: {cmd}. Try /help"));
|
||||
Input::Empty
|
||||
@@ -575,6 +594,13 @@ async fn handle_slash(
|
||||
SlashCommand::Join => cmd_join(session, client).await,
|
||||
SlashCommand::Members => cmd_members(session),
|
||||
SlashCommand::History { count } => cmd_history(session, count),
|
||||
SlashCommand::MeshPeers => cmd_mesh_peers(),
|
||||
SlashCommand::MeshServer { addr } => {
|
||||
display::print_status(&format!(
|
||||
"mesh server hint: reconnect with --server {addr} to use this node"
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
if let Err(e) = result {
|
||||
display::print_error(&format!("{e:#}"));
|
||||
@@ -583,18 +609,49 @@ async fn handle_slash(
|
||||
|
||||
fn print_help() {
|
||||
display::print_status("Commands:");
|
||||
display::print_status(" /dm <user[@domain]> - Start or switch to a DM (federation supported)");
|
||||
display::print_status(" /create-group <name> - Create a new group");
|
||||
display::print_status(" /invite <username> - Invite user to current group");
|
||||
display::print_status(" /remove <username> - Remove a member from the current group");
|
||||
display::print_status(" /leave - Leave the current group");
|
||||
display::print_status(" /join - Join a group from pending Welcome");
|
||||
display::print_status(" /switch <@user|#group> - Switch conversation");
|
||||
display::print_status(" /list - List all conversations");
|
||||
display::print_status(" /members - Show members of current conversation");
|
||||
display::print_status(" /history [N] - Show last N messages (default: 20)");
|
||||
display::print_status(" /whoami - Show your identity");
|
||||
display::print_status(" /quit - Exit");
|
||||
display::print_status(" /dm <user[@domain]> - Start or switch to a DM (federation supported)");
|
||||
display::print_status(" /create-group <name> - Create a new group");
|
||||
display::print_status(" /invite <username> - Invite user to current group");
|
||||
display::print_status(" /remove <username> - Remove a member from the current group");
|
||||
display::print_status(" /leave - Leave the current group");
|
||||
display::print_status(" /join - Join a group from pending Welcome");
|
||||
display::print_status(" /switch <@user|#group> - Switch conversation");
|
||||
display::print_status(" /list - List all conversations");
|
||||
display::print_status(" /members - Show members of current conversation");
|
||||
display::print_status(" /history [N] - Show last N messages (default: 20)");
|
||||
display::print_status(" /whoami - Show your identity");
|
||||
display::print_status(" /mesh peers - Discover nearby qpq nodes via mDNS");
|
||||
display::print_status(" /mesh server <host:port> - Show how to reconnect to a mesh node");
|
||||
display::print_status(" /quit - Exit");
|
||||
}
|
||||
|
||||
/// Discover nearby qpq servers via mDNS (requires `--features mesh` build).
|
||||
fn cmd_mesh_peers() -> anyhow::Result<()> {
|
||||
use super::mesh_discovery::MeshDiscovery;
|
||||
|
||||
match MeshDiscovery::start() {
|
||||
Err(e) => {
|
||||
display::print_error(&format!("mesh discovery: {e}"));
|
||||
return Ok(());
|
||||
}
|
||||
Ok(disc) => {
|
||||
display::print_status("scanning for nearby qpq nodes (2s)...");
|
||||
// Block briefly to collect mDNS announcements from the local network.
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
let peers = disc.peers();
|
||||
if peers.is_empty() {
|
||||
display::print_status("no qpq nodes found on the local network");
|
||||
} else {
|
||||
display::print_status(&format!("found {} node(s):", peers.len()));
|
||||
for p in &peers {
|
||||
display::print_status(&format!(" {} at {}", p.domain, p.server_addr));
|
||||
}
|
||||
display::print_status("use: /mesh server <host:port> to note the address,");
|
||||
display::print_status("then reconnect with: qpq --server <host:port>");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_whoami(session: &SessionState) -> anyhow::Result<()> {
|
||||
@@ -725,9 +782,23 @@ async fn cmd_dm(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create server-side channel.
|
||||
// Create or look up the server-side channel.
|
||||
// was_new=true → this call created the channel; we are the MLS initiator.
|
||||
// was_new=false → channel already existed; peer is the MLS initiator and has
|
||||
// sent (or will send) us a Welcome. Wait for try_auto_join.
|
||||
display::print_status("creating channel...");
|
||||
let channel_id = create_channel(client, &peer_key).await?;
|
||||
let (channel_id, was_new) = create_channel(client, &peer_key).await?;
|
||||
|
||||
if !was_new {
|
||||
// Peer is the MLS initiator. Their Welcome is en route; the background
|
||||
// poller's try_auto_join will process it within the next poll interval
|
||||
// and auto-switch to the conversation automatically.
|
||||
display::print_status(&format!(
|
||||
"DM channel with @{username} exists — peer is initiator, auto-joining via Welcome (arrives within ~1 s)"
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let conv_id = ConversationId::from_slice(&channel_id)
|
||||
.context("server returned invalid channel_id length")?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user