SET AUTOOP {ON|OFF} controls whether access members are auto-opped/voiced
on join (on by default). Stored inverted as a settings flag so the
default stays on, and gated in the join handler.
This commit is contained in:
parent
2c282d036f
commit
2850620be1
7 changed files with 46 additions and 3 deletions
|
|
@ -756,6 +756,8 @@ pub enum ChanSetting {
|
||||||
TopicLock,
|
TopicLock,
|
||||||
// Kick users without channel access when they join.
|
// Kick users without channel access when they join.
|
||||||
Restricted,
|
Restricted,
|
||||||
|
// Auto-op/voice access members on join (on by default).
|
||||||
|
AutoOp,
|
||||||
// BotServ: show members' personal greets on join.
|
// BotServ: show members' personal greets on join.
|
||||||
BotGreet,
|
BotGreet,
|
||||||
// BotServ: forbid the founder from (un)assigning a bot (admin override only).
|
// BotServ: forbid the founder from (un)assigning a bot (admin override only).
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const TOPICS: &[HelpEntry] = &[
|
||||||
HelpEntry { cmd: "REGISTER", summary: "register a channel", detail: "Syntax: \x02REGISTER <#channel>\x02\nRegisters a channel to you. You must currently hold ops in it." },
|
HelpEntry { cmd: "REGISTER", summary: "register a channel", detail: "Syntax: \x02REGISTER <#channel>\x02\nRegisters a channel to you. You must currently hold ops in it." },
|
||||||
HelpEntry { cmd: "INFO", summary: "show channel information", detail: "Syntax: \x02INFO <#channel>\x02\nShows a channel's registration and settings." },
|
HelpEntry { cmd: "INFO", summary: "show channel information", detail: "Syntax: \x02INFO <#channel>\x02\nShows a channel's registration and settings." },
|
||||||
HelpEntry { cmd: "LIST", summary: "list registered channels", detail: "Syntax: \x02LIST\x02\nLists registered channels." },
|
HelpEntry { cmd: "LIST", summary: "list registered channels", detail: "Syntax: \x02LIST\x02\nLists registered channels." },
|
||||||
HelpEntry { cmd: "SET", summary: "change founder or settings", detail: "Syntax: \x02SET <#channel> FOUNDER <account> | DESC <text> | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|RESTRICTED|KEEPTOPIC|TOPICLOCK {ON|OFF}\x02\nTransfers the founder or changes a channel setting." },
|
HelpEntry { cmd: "SET", summary: "change founder or settings", detail: "Syntax: \x02SET <#channel> FOUNDER <account> | DESC <text> | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\x02\nTransfers the founder or changes a channel setting." },
|
||||||
HelpEntry { cmd: "ACCESS", summary: "manage the access list", detail: "Syntax: \x02ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>\x02\nManages the channel access list." },
|
HelpEntry { cmd: "ACCESS", summary: "manage the access list", detail: "Syntax: \x02ACCESS <#channel> LIST | ADD <account> <op|voice> | DEL <account>\x02\nManages the channel access list." },
|
||||||
HelpEntry { cmd: "FLAGS", summary: "granular per-account flags", detail: "Syntax: \x02FLAGS <#channel> [account [+/-flags]]\x02\nViews or changes granular per-account channel flags." },
|
HelpEntry { cmd: "FLAGS", summary: "granular per-account flags", detail: "Syntax: \x02FLAGS <#channel> [account [+/-flags]]\x02\nViews or changes granular per-account channel flags." },
|
||||||
HelpEntry { cmd: "AOP/SOP/VOP", summary: "tiered access shortcuts", detail: "Syntax: \x02AOP|SOP|VOP <#channel> ADD <account> | DEL <account> | LIST\x02\nTiered shortcuts over ACCESS: AOP and SOP grant op, VOP grants voice." },
|
HelpEntry { cmd: "AOP/SOP/VOP", summary: "tiered access shortcuts", detail: "Syntax: \x02AOP|SOP|VOP <#channel> ADD <account> | DEL <account> | LIST\x02\nTiered shortcuts over ACCESS: AOP and SOP grant op, VOP grants voice." },
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,10 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
|
||||||
Some("PEACE") => toggle(me, from, ctx, db, chan, ChanSetting::Peace, args.get(3).copied()),
|
Some("PEACE") => toggle(me, from, ctx, db, chan, ChanSetting::Peace, args.get(3).copied()),
|
||||||
Some("SECUREOPS") => toggle(me, from, ctx, db, chan, ChanSetting::SecureOps, args.get(3).copied()),
|
Some("SECUREOPS") => toggle(me, from, ctx, db, chan, ChanSetting::SecureOps, args.get(3).copied()),
|
||||||
Some("RESTRICTED") => toggle(me, from, ctx, db, chan, ChanSetting::Restricted, args.get(3).copied()),
|
Some("RESTRICTED") => toggle(me, from, ctx, db, chan, ChanSetting::Restricted, args.get(3).copied()),
|
||||||
|
Some("AUTOOP") => toggle(me, from, ctx, db, chan, ChanSetting::AutoOp, args.get(3).copied()),
|
||||||
Some("KEEPTOPIC") => toggle(me, from, ctx, db, chan, ChanSetting::KeepTopic, args.get(3).copied()),
|
Some("KEEPTOPIC") => toggle(me, from, ctx, db, chan, ChanSetting::KeepTopic, args.get(3).copied()),
|
||||||
Some("TOPICLOCK") => toggle(me, from, ctx, db, chan, ChanSetting::TopicLock, args.get(3).copied()),
|
Some("TOPICLOCK") => toggle(me, from, ctx, db, chan, ChanSetting::TopicLock, args.get(3).copied()),
|
||||||
_ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | SUCCESSOR <account>|OFF | DESC <text> | SIGNKICK {ON|OFF} | PRIVATE {ON|OFF} | PEACE {ON|OFF} | SECUREOPS {ON|OFF} | RESTRICTED {ON|OFF} | KEEPTOPIC {ON|OFF} | TOPICLOCK {ON|OFF}"),
|
_ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | SUCCESSOR <account>|OFF | DESC <text> | SIGNKICK {ON|OFF} | PRIVATE {ON|OFF} | PEACE {ON|OFF} | SECUREOPS {ON|OFF} | RESTRICTED {ON|OFF} | AUTOOP {ON|OFF} | KEEPTOPIC {ON|OFF} | TOPICLOCK {ON|OFF}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +84,7 @@ fn label(setting: ChanSetting) -> &'static str {
|
||||||
ChanSetting::Peace => "PEACE",
|
ChanSetting::Peace => "PEACE",
|
||||||
ChanSetting::SecureOps => "SECUREOPS",
|
ChanSetting::SecureOps => "SECUREOPS",
|
||||||
ChanSetting::Restricted => "RESTRICTED",
|
ChanSetting::Restricted => "RESTRICTED",
|
||||||
|
ChanSetting::AutoOp => "AUTOOP",
|
||||||
ChanSetting::KeepTopic => "KEEPTOPIC",
|
ChanSetting::KeepTopic => "KEEPTOPIC",
|
||||||
ChanSetting::TopicLock => "TOPICLOCK",
|
ChanSetting::TopicLock => "TOPICLOCK",
|
||||||
ChanSetting::BotGreet => "GREET",
|
ChanSetting::BotGreet => "GREET",
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ impl Db {
|
||||||
ChanSetting::Peace => settings.peace = on,
|
ChanSetting::Peace => settings.peace = on,
|
||||||
ChanSetting::SecureOps => settings.secureops = on,
|
ChanSetting::SecureOps => settings.secureops = on,
|
||||||
ChanSetting::Restricted => settings.restricted = on,
|
ChanSetting::Restricted => settings.restricted = on,
|
||||||
|
ChanSetting::AutoOp => settings.noautoop = !on, // stored inverted (default on)
|
||||||
ChanSetting::KeepTopic => settings.keeptopic = on,
|
ChanSetting::KeepTopic => settings.keeptopic = on,
|
||||||
ChanSetting::TopicLock => settings.topiclock = on,
|
ChanSetting::TopicLock => settings.topiclock = on,
|
||||||
ChanSetting::BotGreet => settings.bot_greet = on,
|
ChanSetting::BotGreet => settings.bot_greet = on,
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,10 @@ pub struct ChanSettings {
|
||||||
// Kick anyone without channel access when they join.
|
// Kick anyone without channel access when they join.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub restricted: bool,
|
pub restricted: bool,
|
||||||
|
// Stored inverted so it defaults to auto-op ON: when set, access members are
|
||||||
|
// NOT auto-opped on join (they must UP). SET AUTOOP toggles it.
|
||||||
|
#[serde(default)]
|
||||||
|
pub noautoop: bool,
|
||||||
// Remember the topic and restore it when the channel is recreated.
|
// Remember the topic and restore it when the channel is recreated.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub keeptopic: bool,
|
pub keeptopic: bool,
|
||||||
|
|
|
||||||
|
|
@ -826,13 +826,17 @@ impl Engine {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// AUTOOP (on by default): whether access members are auto-opped here.
|
||||||
|
let autoop = self.db.channel(&channel).is_none_or(|c| !c.settings.noautoop);
|
||||||
match mode {
|
match mode {
|
||||||
// A user with access gets their status mode, plus the entry message.
|
// A user with access gets their status mode, plus the entry message.
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
if let Some(msg) = entrymsg {
|
if let Some(msg) = entrymsg {
|
||||||
out.push(NetAction::Notice { from: from.clone(), to: uid.clone(), text: msg });
|
out.push(NetAction::Notice { from: from.clone(), to: uid.clone(), text: msg });
|
||||||
}
|
}
|
||||||
out.push(NetAction::ChannelMode { from, channel, modes: format!("{m} {uid}") });
|
if autoop {
|
||||||
|
out.push(NetAction::ChannelMode { from, channel, modes: format!("{m} {uid}") });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// No access: an auto-kick match is banned and kicked, else greeted.
|
// No access: an auto-kick match is banned and kicked, else greeted.
|
||||||
None => {
|
None => {
|
||||||
|
|
|
||||||
|
|
@ -1162,6 +1162,36 @@
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::Kick { uid, channel, .. } if uid == "000AAAAAC" && channel == "#c")), "no-access user kicked: {out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::Kick { uid, channel, .. } if uid == "000AAAAAC" && channel == "#c")), "no-access user kicked: {out:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChanServ SET AUTOOP OFF suppresses auto-op on join (on by default).
|
||||||
|
#[test]
|
||||||
|
fn chanserv_autoop_off_suppresses_status() {
|
||||||
|
use echo_chanserv::ChanServ;
|
||||||
|
use echo_nickserv::NickServ;
|
||||||
|
let path = std::env::temp_dir().join("echo-autoop.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("#c", "alice").unwrap();
|
||||||
|
let mut e = Engine::new(
|
||||||
|
vec![
|
||||||
|
Box::new(NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }),
|
||||||
|
Box::new(ChanServ { uid: "42SAAAAAB".into() }),
|
||||||
|
],
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
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() });
|
||||||
|
let opped = |out: &[NetAction]| out.iter().any(|a| matches!(a, NetAction::ChannelMode { modes, .. } if modes == "+o 000AAAAAB"));
|
||||||
|
|
||||||
|
// Default: the founder is auto-opped on join.
|
||||||
|
assert!(opped(&e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#c".into(), op: false })), "auto-op on by default");
|
||||||
|
e.handle(NetEvent::Part { uid: "000AAAAAB".into(), channel: "#c".into() });
|
||||||
|
// Turn AUTOOP off; now a join is not auto-opped.
|
||||||
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAB".into(), text: "SET #c AUTOOP OFF".into() });
|
||||||
|
assert!(!opped(&e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#c".into(), op: false })), "no auto-op when AUTOOP is off");
|
||||||
|
}
|
||||||
|
|
||||||
// BotServ BOT ADD/LIST is oper-gated (Priv::Admin) and the bot is remembered.
|
// BotServ BOT ADD/LIST is oper-gated (Priv::Admin) and the bot is remembered.
|
||||||
#[test]
|
#[test]
|
||||||
fn botserv_bot_add_list_is_oper_gated() {
|
fn botserv_bot_add_list_is_oper_gated() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue