diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 92e7aea..307f7eb 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -170,6 +170,16 @@ impl Engine { self.network.user_nick_change(&uid, nick); Vec::new() } + // A registered channel just (re)appeared: re-assert +r. Fires on + // creation and on the uplink burst, so registered channels regain the + // mode after emptying, and after a services relink. + NetEvent::ChannelCreate { channel } => { + if self.db.channel(&channel).is_some() { + vec![NetAction::ChannelMode { channel, modes: "+r".to_string() }] + } else { + Vec::new() + } + } NetEvent::Quit { uid } => { self.network.user_quit(&uid); self.sasl_sessions.remove(&uid); // drop any half-finished exchange @@ -938,4 +948,21 @@ mod tests { assert!(notice(&out, "dropped")); assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#room" && modes == "-r")), "drop clears +r: {out:?}"); } + + // A registered channel (re)appearing re-asserts +r; an unregistered one does not. + #[test] + fn channel_create_reasserts_registered_mode() { + let path = std::env::temp_dir().join("fedserv-chancreate.jsonl"); + let _ = std::fs::remove_file(&path); + let mut db = Db::open(&path, "42S"); + db.register_channel("#reg", "alice").unwrap(); + let ns = NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }; + let mut e = Engine::new(vec![Box::new(ns)], db); + + let out = e.handle(NetEvent::ChannelCreate { channel: "#reg".into() }); + assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#reg" && modes == "+r")), "registered channel regains +r: {out:?}"); + + let out = e.handle(NetEvent::ChannelCreate { channel: "#other".into() }); + assert!(out.is_empty(), "unregistered channel is left alone: {out:?}"); + } } diff --git a/src/proto/inspircd.rs b/src/proto/inspircd.rs index 717390f..2ac3401 100644 --- a/src/proto/inspircd.rs +++ b/src/proto/inspircd.rs @@ -90,6 +90,11 @@ impl Protocol for InspIrcd { } _ => vec![], }, + // FJOIN [params] : — channel create/burst. + "FJOIN" => match tokens.next() { + Some(chan) if !chan.is_empty() => vec![NetEvent::ChannelCreate { channel: chan.to_string() }], + _ => vec![], + }, "QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default() }], // account-registration relay from an ircd: // ACCTREGISTER : @@ -232,4 +237,14 @@ mod tests { "{ev:?}" ); } + + // FJOIN (channel create/burst) surfaces as ChannelCreate for the channel. + #[test] + fn parses_fjoin_as_channel_create() { + let ev = proto().parse(":0IR FJOIN #chan 1783845132 +tn :o,0IRAAAAAB:0"); + assert!( + matches!(ev.as_slice(), [NetEvent::ChannelCreate { channel }] if channel == "#chan"), + "{ev:?}" + ); + } } diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 8bee716..adbdbdc 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -12,6 +12,9 @@ pub enum NetEvent { Privmsg { from: String, to: String, text: String }, UserConnect { uid: String, nick: String }, NickChange { uid: String, nick: String }, + // A channel was created or bursted (InspIRCd FJOIN). Subsequent single joins + // arrive as IJOIN and are not surfaced. + ChannelCreate { channel: String }, Quit { uid: 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 },