diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 41a637c..92e7aea 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -922,16 +922,20 @@ mod tests { // Not identified yet: refused. assert!(notice(&to_cs(&mut e, "000AAAAAB", "REGISTER #room"), "must be identified")); - // Identify, then register. + // Identify, then register: sets +r on the channel. e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() }); - assert!(notice(&to_cs(&mut e, "000AAAAAB", "REGISTER #room"), "now registered")); + let out = to_cs(&mut e, "000AAAAAB", "REGISTER #room"); + assert!(notice(&out, "now registered")); + assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#room" && modes == "+r")), "register sets +r: {out:?}"); assert!(notice(&to_cs(&mut e, "000AAAAAB", "INFO #room"), "alice")); // A different, unidentified user cannot drop it. e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into() }); assert!(notice(&to_cs(&mut e, "000AAAAAC", "DROP #room"), "founder")); - // The founder can. - assert!(notice(&to_cs(&mut e, "000AAAAAB", "DROP #room"), "dropped")); + // The founder can, which also clears +r. + let out = to_cs(&mut e, "000AAAAAB", "DROP #room"); + assert!(notice(&out, "dropped")); + assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes } if channel == "#room" && modes == "-r")), "drop clears +r: {out:?}"); } } diff --git a/src/engine/service.rs b/src/engine/service.rs index 09a57b5..669f52b 100644 --- a/src/engine/service.rs +++ b/src/engine/service.rs @@ -74,4 +74,12 @@ impl ServiceCtx { nick: nick.to_string(), }); } + + // Set channel modes from services, e.g. "+r" on a registered channel. + pub fn channel_mode(&mut self, channel: &str, modes: &str) { + self.actions.push(NetAction::ChannelMode { + channel: channel.to_string(), + modes: modes.to_string(), + }); + } } diff --git a/src/proto/inspircd.rs b/src/proto/inspircd.rs index ed35884..717390f 100644 --- a/src/proto/inspircd.rs +++ b/src/proto/inspircd.rs @@ -179,6 +179,12 @@ impl Protocol for InspIrcd { let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(self.ts); vec![self.from_us(format!("SVSNICK {} {} {}", uid, nick, now))] } + // FMODE . The ircd drops an FMODE whose TS is newer + // than the channel's, so we send TS 1 to guarantee it applies to the + // existing channel. Sourced from our server, which +r requires. + NetAction::ChannelMode { channel, modes } => { + vec![self.from_us(format!("FMODE {} 1 {}", channel, modes))] + } NetAction::Raw(s) => vec![s.clone()], // Internal: the link layer handles this before serialization. NetAction::DeferRegister { .. } => vec![], diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 43271f5..8bee716 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -38,6 +38,9 @@ pub enum NetAction { // Force a user's nick (SVSNICK), e.g. renaming to a guest nick on logout. // The protocol stamps the new nick's timestamp. ForceNick { uid: String, nick: String }, + // Set channel modes from services, e.g. +r on a registered channel. The + // protocol stamps a timestamp the ircd will accept. + ChannelMode { channel: String, modes: String }, Raw(String), // Internal only, never serialized to the wire: a registration whose password // still needs its (expensive) key derivation. The link layer runs the diff --git a/src/services/chanserv.rs b/src/services/chanserv.rs index 365db3e..fc88239 100644 --- a/src/services/chanserv.rs +++ b/src/services/chanserv.rs @@ -34,7 +34,10 @@ impl Service for ChanServ { return; }; match db.register_channel(chan, account) { - Ok(()) => ctx.notice(me, from.uid, format!("Channel \x02{chan}\x02 is now registered to \x02{account}\x02.")), + Ok(()) => { + ctx.channel_mode(chan, "+r"); // mark the channel registered + ctx.notice(me, from.uid, format!("Channel \x02{chan}\x02 is now registered to \x02{account}\x02.")); + } Err(ChanError::Exists) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 is already registered.")), Err(_) => ctx.notice(me, from.uid, "Registration failed, please try again later."), } @@ -67,7 +70,10 @@ impl Service for ChanServ { return; } match db.drop_channel(chan) { - Ok(()) => ctx.notice(me, from.uid, format!("Channel \x02{chan}\x02 has been dropped.")), + Ok(()) => { + ctx.channel_mode(chan, "-r"); // no longer registered + ctx.notice(me, from.uid, format!("Channel \x02{chan}\x02 has been dropped.")); + } Err(_) => ctx.notice(me, from.uid, "Drop failed, please try again later."), } }