diff --git a/api/src/lib.rs b/api/src/lib.rs index 7624cc7..8bea31f 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -46,6 +46,10 @@ pub enum NetEvent { // A user was forcibly removed (KILL). Handled like a quit, except a killed // services bot is reintroduced rather than forgotten. UserKilled { uid: String }, + // A server split away (SQUIT). `server` is its SID; every user behind it + // (whose uid carries that SID prefix) is gone, since a split is signalled once + // rather than as a QUIT per user. + ServerSplit { server: String }, // An ircd relaying an IRCv3 account-registration request to us as the authority. AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String }, // An ircd relaying a SASL exchange step to us (the SASL agent). mode = H/S/C/D. diff --git a/modules/protocol/inspircd/src/lib.rs b/modules/protocol/inspircd/src/lib.rs index 6735596..5f18119 100644 --- a/modules/protocol/inspircd/src/lib.rs +++ b/modules/protocol/inspircd/src/lib.rs @@ -208,6 +208,11 @@ impl Protocol for InspIrcd { Some(uid) if !uid.is_empty() => vec![NetEvent::UserKilled { uid: uid.to_string() }], _ => vec![], }, + // : SQUIT : — a server split; forget its users. + "SQUIT" => match tokens.next() { + Some(server) if !server.is_empty() => vec![NetEvent::ServerSplit { server: server.to_string() }], + _ => vec![], + }, // ENCAP … — we care about relayed SASL and the // account-registration relay (the ircd's account module forwards a // leaf's REGISTER/VERIFY/RESEND/STATUS to us, the authority server). @@ -478,6 +483,13 @@ mod tests { assert!(matches!(ev.as_slice(), [NetEvent::UserKilled { uid }] if uid == "0IRAAAAAB"), "{ev:?}"); } + // A SQUIT surfaces as a ServerSplit carrying the departed server's SID. + #[test] + fn parses_squit() { + let ev = proto().parse(":42S SQUIT 0IR :ping timeout"); + assert!(matches!(ev.as_slice(), [NetEvent::ServerSplit { server }] if server == "0IR"), "{ev:?}"); + } + // A UID burst introduces the user under their current nick. #[test] fn parses_uid_burst() { diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 679f501..e374746 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1057,6 +1057,14 @@ impl Engine { self.forget_user(&uid); Vec::new() } + NetEvent::ServerSplit { server } => { + // Every user behind the departed server is gone in one message; + // forget them so their sessions, slots and memberships don't linger. + for uid in self.network.uids_on_server(&server) { + self.forget_user(&uid); + } + Vec::new() + } NetEvent::Privmsg { from, to, text } => self.dispatch(&from, &to, &text), NetEvent::AccountRequest { reqid, origin, kind, account, p2, p3 } => { self.account_request(reqid, origin, kind, account, p2, p3) diff --git a/src/engine/state.rs b/src/engine/state.rs index c627c0f..12824e6 100644 --- a/src/engine/state.rs +++ b/src/engine/state.rs @@ -180,6 +180,12 @@ impl Network { self.users.get(uid).map(|u| u.host.as_str()) } + // Every known user whose uid carries `sid` as its prefix — i.e. those behind + // a server, used to forget them all when it splits (SQUIT). + pub fn uids_on_server(&self, sid: &str) -> Vec { + self.users.keys().filter(|u| u.starts_with(sid)).cloned().collect() + } + pub fn user_nick_change(&mut self, uid: &str, nick: String) { if let Some(user) = self.users.get_mut(uid) { self.seen.insert(lc(&nick), Seen { nick: nick.clone(), ts: now(), what: format!("changing nick from {}", user.nick) }); diff --git a/src/engine/tests.rs b/src/engine/tests.rs index e12609c..7e66e54 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -432,6 +432,25 @@ assert!(parted, "the assigned bot parts the released channel"); } + // A server split (SQUIT) forgets exactly the users behind it (uids carrying + // that SID), leaving users on other servers untouched. + #[test] + fn squit_forgets_users_behind_the_split() { + let mut e = engine_with("squit", "alice", "sesame"); + e.db.register("bob", "hunter2", None).unwrap(); + e.handle(NetEvent::UserConnect { uid: "0IRAAAAAB".into(), nick: "alice".into(), host: "h".into() , ip: "0.0.0.0".into() }); + e.handle(NetEvent::UserConnect { uid: "0XYAAAAAB".into(), nick: "bob".into(), host: "h".into() , ip: "0.0.0.0".into() }); + e.handle(NetEvent::Privmsg { from: "0IRAAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() }); + e.handle(NetEvent::Privmsg { from: "0XYAAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY hunter2".into() }); + assert_eq!(e.network.account_of("0IRAAAAAB"), Some("alice")); + assert_eq!(e.network.account_of("0XYAAAAAB"), Some("bob")); + + // Server 0IR splits away. + e.handle(NetEvent::ServerSplit { server: "0IR".into() }); + assert!(e.network.account_of("0IRAAAAAB").is_none(), "users behind the split are forgotten"); + assert_eq!(e.network.account_of("0XYAAAAAB"), Some("bob"), "users on other servers are kept"); + } + // A netburst that replays a user's accountname restores their services login // (so a services restart doesn't silently log everyone out); empty = logout. #[test]