Re-assert +r when a registered channel is created

fedserv now handles FJOIN (channel create/burst) and sets +r on any
channel that is registered, so a registered channel regains the mode
after it empties and is recreated, and after a services relink. Single
joins arrive as IJOIN and are ignored.
This commit is contained in:
Jean Chevronnet 2026-07-12 08:41:45 +00:00
parent 64500906d8
commit eb22612da8
No known key found for this signature in database
3 changed files with 45 additions and 0 deletions

View file

@ -170,6 +170,16 @@ impl Engine {
self.network.user_nick_change(&uid, nick); self.network.user_nick_change(&uid, nick);
Vec::new() 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 } => { NetEvent::Quit { uid } => {
self.network.user_quit(&uid); self.network.user_quit(&uid);
self.sasl_sessions.remove(&uid); // drop any half-finished exchange self.sasl_sessions.remove(&uid); // drop any half-finished exchange
@ -938,4 +948,21 @@ mod tests {
assert!(notice(&out, "dropped")); assert!(notice(&out, "dropped"));
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#room" && modes == "-r")), "drop clears +r: {out:?}"); 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:?}");
}
} }

View file

@ -90,6 +90,11 @@ impl Protocol for InspIrcd {
} }
_ => vec![], _ => vec![],
}, },
// FJOIN <chan> <ts> <modes> [params] :<members> — 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() }], "QUIT" => vec![NetEvent::Quit { uid: source.unwrap_or_default() }],
// account-registration relay from an ircd: // account-registration relay from an ircd:
// ACCTREGISTER <reqid> <origin> <kind> <account> <p2> :<p3> // ACCTREGISTER <reqid> <origin> <kind> <account> <p2> :<p3>
@ -232,4 +237,14 @@ mod tests {
"{ev:?}" "{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:?}"
);
}
} }

View file

@ -12,6 +12,9 @@ pub enum NetEvent {
Privmsg { from: String, to: String, text: String }, Privmsg { from: String, to: String, text: String },
UserConnect { uid: String, nick: String }, UserConnect { uid: String, nick: String },
NickChange { 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 }, Quit { uid: String },
// An ircd relaying an IRCv3 account-registration request to us as the authority. // 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 }, AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String },