From dbc45a0b6afe4a502cf12222e3d69d6a70058bdf Mon Sep 17 00:00:00 2001 From: Jean Date: Sun, 12 Jul 2026 10:25:01 +0000 Subject: [PATCH] Add ChanServ MODE command MODE <#channel> lets the founder set channel modes through ChanServ (sourced from ChanServ). The mode string is passed through, so simple modes and masks work; founder-gated. --- src/engine/mod.rs | 7 ++++++- src/services/chanserv.rs | 26 +++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/engine/mod.rs b/src/engine/mod.rs index c3fc130..6f3ed06 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -968,8 +968,13 @@ mod tests { assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#room" && modes == "+rnt-s")), "mlock applied: {out:?}"); assert!(notice(&to_cs(&mut e, "000AAAAAB", "MLOCK #room"), "+nt-s")); - // A different, unidentified user cannot set the lock or drop it. + // Founder sets modes directly via ChanServ MODE. + let out = to_cs(&mut e, "000AAAAAB", "MODE #room +S"); + assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#room" && modes == "+S")), "MODE applies: {out:?}"); + + // A different, unidentified user cannot change modes, set the lock, or drop it. e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into() }); + assert!(notice(&to_cs(&mut e, "000AAAAAC", "MODE #room +i"), "founder")); assert!(notice(&to_cs(&mut e, "000AAAAAC", "MLOCK #room +i"), "founder")); assert!(notice(&to_cs(&mut e, "000AAAAAC", "DROP #room"), "founder")); diff --git a/src/services/chanserv.rs b/src/services/chanserv.rs index be3d3da..18d3667 100644 --- a/src/services/chanserv.rs +++ b/src/services/chanserv.rs @@ -123,7 +123,31 @@ impl Service for ChanServ { Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."), } } - Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02 <#channel>, \x02INFO\x02 <#channel>, \x02MLOCK\x02 <#channel> [modes], \x02DROP\x02 <#channel>."), + Some("MODE") => { + let Some(&chan) = args.get(1) else { + ctx.notice(me, from.uid, "Syntax: MODE <#channel> , e.g. MODE #chan +nt"); + return; + }; + if args.len() <= 2 { + ctx.notice(me, from.uid, "Syntax: MODE <#channel> , e.g. MODE #chan +nt"); + return; + } + let founder = match db.channel(chan) { + Some(info) => info.founder.clone(), + None => { + ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); + return; + } + }; + if from.account != Some(founder.as_str()) { + ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can change its modes.")); + return; + } + let modes = args[2..].join(" "); + ctx.channel_mode(me, chan, &modes); + ctx.notice(me, from.uid, format!("Set \x02{modes}\x02 on \x02{chan}\x02.")); + } + Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02 <#channel>, \x02INFO\x02 <#channel>, \x02MODE\x02 <#channel> , \x02MLOCK\x02 <#channel> [modes], \x02DROP\x02 <#channel>."), Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), None => {} }