diff --git a/api/src/lib.rs b/api/src/lib.rs index 9ac056d..d27c618 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -69,6 +69,9 @@ pub enum NetEvent { // The ircd's live channel-mode set from its `CAPAB CHANMODES` burst — mode // letters, param arity, and prefix (status) modes incl custom ones like `Y`. ChanModeRegistry { modes: Vec }, + // The ircd's `CASEMAPPING` from its `CAPAB CAPABILITIES` burst. echo folds + // identifiers as ascii, so it verifies this matches and warns otherwise. + Casemapping { name: String }, // A server split away (SQUIT). `server` is its SID; every user behind it — and // behind any server in its subtree — is gone, since a split is signalled once // rather than as a QUIT per user. diff --git a/modules/protocol/inspircd/src/lib.rs b/modules/protocol/inspircd/src/lib.rs index 6ffa180..7c28dcc 100644 --- a/modules/protocol/inspircd/src/lib.rs +++ b/modules/protocol/inspircd/src/lib.rs @@ -330,6 +330,13 @@ impl Protocol for InspIrcd { vec![NetEvent::ChanModeRegistry { modes }] } } + // CAPAB CAPABILITIES :K=V … — a KV list; we care about CASEMAPPING. + Some(s) if s.eq_ignore_ascii_case("CAPABILITIES") => { + match rest.split_whitespace().find_map(|t| t.trim_start_matches(':').strip_prefix("CASEMAPPING=")) { + Some(name) => vec![NetEvent::Casemapping { name: name.to_string() }], + None => vec![], + } + } _ => vec![], }, _ => vec![NetEvent::Unknown { line: line.to_string() }], @@ -656,6 +663,15 @@ mod tests { assert!(p.parse("CAPAB CHANMODES :ban=b").is_empty(), "other CAPAB subcommands ignored"); } + // CAPAB CAPABILITIES surfaces CASEMAPPING (echo verifies it's ascii). + #[test] + fn parses_capab_casemapping() { + let mut p = proto(); + assert!(matches!(p.parse("CAPAB CAPABILITIES :NICKMAX=31 CASEMAPPING=ascii CHANMAX=65").as_slice(), + [NetEvent::Casemapping { name }] if name == "ascii")); + assert!(p.parse("CAPAB CAPABILITIES :NICKMAX=31 CHANMAX=65").is_empty(), "no CASEMAPPING token -> nothing"); + } + // CAPAB CHANMODES surfaces mode arity + prefix modes, including a custom prefix // like ojoin's Y (symbol !, rank 9000000) that the hardcoded set doesn't know. #[test] diff --git a/src/engine/mod.rs b/src/engine/mod.rs index b0a4b2a..2620011 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1116,6 +1116,16 @@ impl Engine { self.db.set_live_chanmodes(modes); Vec::new() } + NetEvent::Casemapping { name } => { + // echo folds identifiers as ascii. Verify the ircd agrees, rather than + // assuming it silently — a mismatch would desync account/channel identity. + if name.eq_ignore_ascii_case("ascii") { + tracing::info!(casemapping = %name, "ircd casemapping matches echo (ascii)"); + } else { + tracing::warn!(casemapping = %name, "ircd CASEMAPPING is not ascii — echo compares identifiers as ascii, so nicks/channels differing only by rfc1459-equivalent characters may be mismatched; keep the ircd on CASEMAPPING=ascii"); + } + Vec::new() + } NetEvent::UserConnect { uid, nick, host, ip } => { let arriving_nick = nick.clone(); self.network.user_connect(uid.clone(), nick, host, ip.clone());