diff --git a/modules/operserv/src/lib.rs b/modules/operserv/src/lib.rs index 67cf95d..0c1fb11 100644 --- a/modules/operserv/src/lib.rs +++ b/modules/operserv/src/lib.rs @@ -13,6 +13,8 @@ mod redact; mod forbid; #[path = "protect.rs"] mod protect; +#[path = "setall.rs"] +mod setall; mod notify; #[path = "global.rs"] mod global; @@ -85,6 +87,7 @@ impl Service for OperServ { Some(cmd) if cmd.eq_ignore_ascii_case("REDACT") => redact::handle(me, from, args, ctx), Some(cmd) if cmd.eq_ignore_ascii_case("FORBID") => forbid::handle(me, from, args, ctx, db), Some(cmd) if cmd.eq_ignore_ascii_case("PROTECT") => protect::handle(me, from, args, ctx, db), + Some(cmd) if cmd.eq_ignore_ascii_case("SETALL") => setall::handle(me, from, args, ctx, db), Some(cmd) if cmd.eq_ignore_ascii_case("NOTIFY") => notify::handle(me, from, args, ctx, db), Some(cmd) if cmd.eq_ignore_ascii_case("GLOBAL") => global::handle(me, from, args, ctx), Some(cmd) if cmd.eq_ignore_ascii_case("KILL") => kill::handle(me, from, args, ctx, net), @@ -122,6 +125,7 @@ const TOPICS: &[HelpEntry] = &[ HelpEntry { cmd: "REDACT", summary: "delete a message by its msgid", detail: "Syntax: \x02REDACT <#channel|nick> [reason]\x02\nDeletes a specific message (by its IRCv3 \x02msgid\x02, which your client shows) from everyone who received it. draft/message-redaction." }, HelpEntry { cmd: "FORBID", summary: "ban a nick/chan/email from registration", detail: "Syntax: \x02FORBID ADD | FORBID DEL | FORBID LIST\x02\nStops a nick, channel, or email pattern from being registered." }, HelpEntry { cmd: "PROTECT", summary: "turn nick protection on/off for accounts", detail: "Syntax: \x02PROTECT ALL | [ON|OFF]\x02\nToggles the identify-or-be-renamed enforcement. \x02ALL\x02 enables it on every account that has it off (e.g. after a migration). Admin only." }, + HelpEntry { cmd: "SETALL", summary: "apply a channel setting to every channel", detail: "Syntax: \x02SETALL \x02\nApplies one on/off ChanServ setting to every registered channel, overriding their own choice — for a network-wide policy roll-out. Admin only." }, HelpEntry { cmd: "NOTIFY", summary: "watch masks and log their events", detail: "Syntax: \x02NOTIFY ADD + | NOTIFY DEL | NOTIFY LIST [pattern] | NOTIFY VIEW [pattern] | NOTIFY CLEAR\x02\nWatches a \x02nick!user@host\x02 pattern, bare nick glob, or \x02#channel\x02; matching users have their flagged events announced to the staff feed. Flags: \x02c\x02 connect, \x02d\x02 disconnect, \x02o\x02 oper-up, \x02j\x02 join, \x02p\x02 part, \x02k\x02 kick, \x02m\x02 channel mode, \x02t\x02 topic, \x02n\x02 nick change, \x02u\x02 user mode, \x02s\x02 services command, \x02S\x02 services SET (\x02*\x02 for all). Expiry like \x02+30d\x02, or \x02+0\x02 for permanent. Admin only." }, HelpEntry { cmd: "SQLINE", summary: "ban a nick pattern", detail: "Syntax: \x02SQLINE ADD [+expiry] | SQLINE DEL | SQLINE LIST [pattern]\x02\nBans matching nicknames." }, HelpEntry { cmd: "SNLINE", summary: "ban a realname pattern", detail: "Syntax: \x02SNLINE ADD [+expiry] | SNLINE DEL | SNLINE LIST [pattern]\x02\nBans matching real names." }, diff --git a/modules/operserv/src/setall.rs b/modules/operserv/src/setall.rs new file mode 100644 index 0000000..ab58d54 --- /dev/null +++ b/modules/operserv/src/setall.rs @@ -0,0 +1,47 @@ +use echo_api::{ChanSetting, Priv, Sender, ServiceCtx, Store}; + +// SETALL : apply one channel on/off setting to EVERY registered +// channel at once — useful for rolling out a network-wide policy change. This +// overrides each channel's own choice, so it's admin-only and announced to staff. +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { + if !from.privs.has(Priv::Admin) { + ctx.notice(me, from.uid, "Access denied — SETALL needs the \x02admin\x02 privilege."); + return; + } + let syntax = "Syntax: SETALL "; + let (Some(name), Some(state)) = (args.get(1), args.get(2)) else { + ctx.notice(me, from.uid, syntax); + return; + }; + let setting = match name.to_ascii_uppercase().as_str() { + "SECUREOPS" => ChanSetting::SecureOps, + "SECUREVOICES" => ChanSetting::SecureVoices, + "PEACE" => ChanSetting::Peace, + "KEEPTOPIC" => ChanSetting::KeepTopic, + "TOPICLOCK" => ChanSetting::TopicLock, + "RESTRICTED" => ChanSetting::Restricted, + "PRIVATE" => ChanSetting::Private, + _ => { + ctx.notice(me, from.uid, syntax); + return; + } + }; + let on = match state.to_ascii_uppercase().as_str() { + "ON" => true, + "OFF" => false, + _ => { + ctx.notice(me, from.uid, syntax); + return; + } + }; + let mut n = 0; + for c in db.channels() { + if db.set_channel_setting(&c.name, setting, on).is_ok() { + n += 1; + } + } + let up = name.to_ascii_uppercase(); + let word = if on { "on" } else { "off" }; + ctx.notice(me, from.uid, format!("Set \x02{up}\x02 \x02{word}\x02 on \x02{n}\x02 channel(s).")); + ctx.alert("OPER", format!("SETALL {up} {word} on {n} channels")); +} diff --git a/src/engine/tests.rs b/src/engine/tests.rs index 2223b73..4dc3609 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -5429,6 +5429,32 @@ assert!(e.db.account_wants_protect("bob"), "bob is now protected after PROTECT ALL"); } + // OperServ SETALL applies a channel setting to every registered channel. + #[test] + fn operserv_setall_applies_to_every_channel() { + use echo_operserv::OperServ; + let path = std::env::temp_dir().join("echo-setall.jsonl"); + let _ = std::fs::remove_file(&path); + let mut db = Db::open(&path, "42S"); + db.scram_iterations = 4096; + db.register("alice", "sesame", None).unwrap(); + db.register_channel("#one", "alice").unwrap(); + db.register_channel("#two", "alice").unwrap(); + assert!(!db.channel("#one").unwrap().settings.secureops); + let ns = NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }; + let os = OperServ { uid: "42SAAAAAH".into() }; + let mut e = Engine::new(vec![Box::new(ns), Box::new(os)], db); + e.set_sid("42S".into()); + let mut opers = std::collections::HashMap::new(); + opers.insert("alice".to_string(), Privs::default().with(echo_api::Priv::Admin)); + e.set_opers(opers); + e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into(), host: "h".into(), ip: "0.0.0.0".into() }); + e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() }); + e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAH".into(), text: "SETALL SECUREOPS ON".into() }); + assert!(e.db.channel("#one").unwrap().settings.secureops, "#one now has secureops"); + assert!(e.db.channel("#two").unwrap().settings.secureops, "#two now has secureops"); + } + // ChanServ SET: description and founder transfer, founder-gated. #[test] fn chanserv_set() {