Add SECUREVOICES: strip voice from users without channel access
All checks were successful
CI / check (push) Successful in 5m20s

This commit is contained in:
Jean Chevronnet 2026-07-20 21:34:09 +00:00
parent b2f075ee00
commit ae90388fbf
No known key found for this signature in database
15 changed files with 62 additions and 8 deletions

View file

@ -1849,6 +1849,7 @@ pub struct ChannelView {
pub private: bool, pub private: bool,
pub peace: bool, pub peace: bool,
pub secureops: bool, pub secureops: bool,
pub securevoices: bool,
pub keeptopic: bool, pub keeptopic: bool,
pub topiclock: bool, pub topiclock: bool,
pub suspended: bool, pub suspended: bool,
@ -1871,6 +1872,7 @@ pub enum ChanSetting {
Private, Private,
Peace, Peace,
SecureOps, SecureOps,
SecureVoices,
KeepTopic, KeepTopic,
TopicLock, TopicLock,
// Kick users without channel access when they join. // Kick users without channel access when they join.

View file

@ -1368,5 +1368,6 @@
"Confirm now": "Jetzt bestätigen", "Confirm now": "Jetzt bestätigen",
"… and \u0002{more}\u0002 more; showing the first {cap}.": "… und \u0002{more}\u0002 weitere; die ersten {cap} werden angezeigt.", "… and \u0002{more}\u0002 more; showing the first {cap}.": "… und \u0002{more}\u0002 weitere; die ersten {cap} werden angezeigt.",
"log in and reclaim your nick": "anmelden und deinen Nick zurückholen", "log in and reclaim your nick": "anmelden und deinen Nick zurückholen",
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Syntax: \u0002LOGIN <Nick> <Passwort>\u0002\nMeldet dich beim Konto von <Nick> an und setzt dich auf diesen Nick, wobei jede Sitzung, die ihn belegt, freigegeben wird." "Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Syntax: \u0002LOGIN <Nick> <Passwort>\u0002\nMeldet dich beim Konto von <Nick> an und setzt dich auf diesen Nick, wobei jede Sitzung, die ihn belegt, freigegeben wird.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nÜberträgt den Gründer oder ändert eine Kanaleinstellung."
} }

View file

@ -1368,5 +1368,6 @@
"Confirm now": "Confirmar ahora", "Confirm now": "Confirmar ahora",
"… and \u0002{more}\u0002 more; showing the first {cap}.": "… y \u0002{more}\u0002 más; mostrando los primeros {cap}.", "… and \u0002{more}\u0002 more; showing the first {cap}.": "… y \u0002{more}\u0002 más; mostrando los primeros {cap}.",
"log in and reclaim your nick": "iniciar sesión y recuperar tu apodo", "log in and reclaim your nick": "iniciar sesión y recuperar tu apodo",
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxis: \u0002LOGIN <apodo> <contraseña>\u0002\nInicia sesión en la cuenta de <apodo> y te mueve a ese apodo, liberando cualquier sesión que ya lo esté usando." "Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxis: \u0002LOGIN <apodo> <contraseña>\u0002\nInicia sesión en la cuenta de <apodo> y te mueve a ese apodo, liberando cualquier sesión que ya lo esté usando.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxis: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfiere el fundador o cambia un ajuste del canal."
} }

View file

@ -1368,5 +1368,6 @@
"Confirm now": "Confirmar ahora", "Confirm now": "Confirmar ahora",
"… and \u0002{more}\u0002 more; showing the first {cap}.": "… y \u0002{more}\u0002 más; mostrando los primeros {cap}.", "… and \u0002{more}\u0002 more; showing the first {cap}.": "… y \u0002{more}\u0002 más; mostrando los primeros {cap}.",
"log in and reclaim your nick": "iniciar sesión y recuperar tu apodo", "log in and reclaim your nick": "iniciar sesión y recuperar tu apodo",
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxis: \u0002LOGIN <apodo> <contraseña>\u0002\nInicia sesión en la cuenta de <apodo> y te mueve a ese apodo, liberando cualquier sesión que ya lo ocupe." "Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxis: \u0002LOGIN <apodo> <contraseña>\u0002\nInicia sesión en la cuenta de <apodo> y te mueve a ese apodo, liberando cualquier sesión que ya lo ocupe.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxis: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfiere el fundador o cambia un ajuste del canal."
} }

View file

@ -1368,5 +1368,6 @@
"Confirm now": "Confirmer maintenant", "Confirm now": "Confirmer maintenant",
"… and \u0002{more}\u0002 more; showing the first {cap}.": "… et \u0002{more}\u0002 de plus ; affichage des {cap} premiers.", "… and \u0002{more}\u0002 more; showing the first {cap}.": "… et \u0002{more}\u0002 de plus ; affichage des {cap} premiers.",
"log in and reclaim your nick": "se connecter et récupérer son pseudo", "log in and reclaim your nick": "se connecter et récupérer son pseudo",
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Syntaxe : \u0002LOGIN <pseudo> <mot de passe>\u0002\nVous connecte au compte de <pseudo> et vous place sur ce pseudo, en libérant toute session qui l'occupe déjà." "Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Syntaxe : \u0002LOGIN <pseudo> <mot de passe>\u0002\nVous connecte au compte de <pseudo> et vous place sur ce pseudo, en libérant toute session qui l'occupe déjà.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Syntaxe : \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfère le fondateur ou modifie un réglage du salon."
} }

View file

@ -1368,5 +1368,6 @@
"Confirm now": "Confirmar agora", "Confirm now": "Confirmar agora",
"… and \u0002{more}\u0002 more; showing the first {cap}.": "… e mais \u0002{more}\u0002; mostrando os primeiros {cap}.", "… and \u0002{more}\u0002 more; showing the first {cap}.": "… e mais \u0002{more}\u0002; mostrando os primeiros {cap}.",
"log in and reclaim your nick": "entrar e recuperar seu apelido", "log in and reclaim your nick": "entrar e recuperar seu apelido",
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxe: \u0002LOGIN <apelido> <senha>\u0002\nEntra na conta de <apelido> e move você para esse apelido, liberando qualquer sessão que já o ocupe." "Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxe: \u0002LOGIN <apelido> <senha>\u0002\nEntra na conta de <apelido> e move você para esse apelido, liberando qualquer sessão que já o ocupe.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxe: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfere o fundador ou altera uma configuração do canal."
} }

View file

@ -1368,5 +1368,6 @@
"Confirm now": "Confirmar agora", "Confirm now": "Confirmar agora",
"… and \u0002{more}\u0002 more; showing the first {cap}.": "… e mais \u0002{more}\u0002; a mostrar os primeiros {cap}.", "… and \u0002{more}\u0002 more; showing the first {cap}.": "… e mais \u0002{more}\u0002; a mostrar os primeiros {cap}.",
"log in and reclaim your nick": "iniciar sessão e recuperar a sua alcunha", "log in and reclaim your nick": "iniciar sessão e recuperar a sua alcunha",
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxe: \u0002LOGIN <alcunha> <palavra-passe>\u0002\nInicia sessão na conta de <alcunha> e move-o para essa alcunha, libertando qualquer sessão que já a ocupe." "Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxe: \u0002LOGIN <alcunha> <palavra-passe>\u0002\nInicia sessão na conta de <alcunha> e move-o para essa alcunha, libertando qualquer sessão que já a ocupe.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxe: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfere o fundador ou altera uma definição do canal."
} }

View file

@ -53,7 +53,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> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|RESTRICTED|AUTOOP|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> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|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|!group> <sop|op|halfop|voice> | DEL <account|!group>\x02\nManages the channel access list. The target may be a GroupServ \x02!group\x02; each of its members holding the group's \x02c\x02 flag then inherits the access." }, HelpEntry { cmd: "ACCESS", summary: "manage the access list", detail: "Syntax: \x02ACCESS <#channel> LIST | ADD <account|!group> <sop|op|halfop|voice> | DEL <account|!group>\x02\nManages the channel access list. The target may be a GroupServ \x02!group\x02; each of its members holding the group's \x02c\x02 flag then inherits the access." },
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: "LEVELS", summary: "tune which tier holds each capability", detail: "Syntax: \x02LEVELS <#channel> [SET <capability> <tier> | RESET <capability>]\x02\nGrants a channel capability (\x02OP\x02, \x02TOPIC\x02, \x02INVITE\x02, \x02ACCESS\x02) to an access tier (\x02VOP\x02/\x02HOP\x02/\x02AOP\x02/\x02SOP\x02) and above, e.g. let halfops manage AKICK. Additive — it never removes access. Founder only." }, HelpEntry { cmd: "LEVELS", summary: "tune which tier holds each capability", detail: "Syntax: \x02LEVELS <#channel> [SET <capability> <tier> | RESET <capability>]\x02\nGrants a channel capability (\x02OP\x02, \x02TOPIC\x02, \x02INVITE\x02, \x02ACCESS\x02) to an access tier (\x02VOP\x02/\x02HOP\x02/\x02AOP\x02/\x02SOP\x02) and above, e.g. let halfops manage AKICK. Additive — it never removes access. Founder only." },
@ -201,6 +201,7 @@ impl Service for ChanServ {
if info.private { opts.push("PRIVATE"); } if info.private { opts.push("PRIVATE"); }
if info.peace { opts.push("PEACE"); } if info.peace { opts.push("PEACE"); }
if info.secureops { opts.push("SECUREOPS"); } if info.secureops { opts.push("SECUREOPS"); }
if info.securevoices { opts.push("SECUREVOICES"); }
if info.keeptopic { opts.push("KEEPTOPIC"); } if info.keeptopic { opts.push("KEEPTOPIC"); }
if info.topiclock { opts.push("TOPICLOCK"); } if info.topiclock { opts.push("TOPICLOCK"); }
if !opts.is_empty() { if !opts.is_empty() {

View file

@ -93,11 +93,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
Some("PRIVATE") => toggle(me, from, ctx, db, chan, ChanSetting::Private, args.get(3).copied()), Some("PRIVATE") => toggle(me, from, ctx, db, chan, ChanSetting::Private, args.get(3).copied()),
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("SECUREVOICES") => toggle(me, from, ctx, db, chan, ChanSetting::SecureVoices, 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("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> | URL [address] | EMAIL [address] | SIGNKICK {ON|OFF} | PRIVATE {ON|OFF} | PEACE {ON|OFF} | SECUREOPS {ON|OFF} | RESTRICTED {ON|OFF} | AUTOOP {ON|OFF} | KEEPTOPIC {ON|OFF} | TOPICLOCK {ON|OFF}"), _ => ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | SUCCESSOR <account>|OFF | DESC <text> | URL [address] | EMAIL [address] | SIGNKICK {ON|OFF} | PRIVATE {ON|OFF} | PEACE {ON|OFF} | SECUREOPS {ON|OFF} | SECUREVOICES {ON|OFF} | RESTRICTED {ON|OFF} | AUTOOP {ON|OFF} | KEEPTOPIC {ON|OFF} | TOPICLOCK {ON|OFF}"),
} }
} }
@ -108,6 +109,7 @@ fn label(setting: ChanSetting) -> &'static str {
ChanSetting::Private => "PRIVATE", ChanSetting::Private => "PRIVATE",
ChanSetting::Peace => "PEACE", ChanSetting::Peace => "PEACE",
ChanSetting::SecureOps => "SECUREOPS", ChanSetting::SecureOps => "SECUREOPS",
ChanSetting::SecureVoices => "SECUREVOICES",
ChanSetting::Restricted => "RESTRICTED", ChanSetting::Restricted => "RESTRICTED",
ChanSetting::AutoOp => "AUTOOP", ChanSetting::AutoOp => "AUTOOP",
ChanSetting::KeepTopic => "KEEPTOPIC", ChanSetting::KeepTopic => "KEEPTOPIC",

View file

@ -274,6 +274,7 @@ impl Db {
ChanSetting::Private => settings.private = on, ChanSetting::Private => settings.private = on,
ChanSetting::Peace => settings.peace = on, ChanSetting::Peace => settings.peace = on,
ChanSetting::SecureOps => settings.secureops = on, ChanSetting::SecureOps => settings.secureops = on,
ChanSetting::SecureVoices => settings.securevoices = on,
ChanSetting::Restricted => settings.restricted = on, ChanSetting::Restricted => settings.restricted = on,
ChanSetting::AutoOp => settings.noautoop = !on, // stored inverted (default on) ChanSetting::AutoOp => settings.noautoop = !on, // stored inverted (default on)
ChanSetting::KeepTopic => settings.keeptopic = on, ChanSetting::KeepTopic => settings.keeptopic = on,

View file

@ -428,6 +428,9 @@ pub struct ChanSettings {
// Strip channel-operator status from anyone without op-level access. // Strip channel-operator status from anyone without op-level access.
#[serde(default)] #[serde(default)]
pub secureops: bool, pub secureops: bool,
// Strip voice from anyone without voice-level (or higher) access.
#[serde(default)]
pub securevoices: bool,
// 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,

View file

@ -653,6 +653,7 @@ fn channel_view(c: &ChannelInfo) -> ChannelView {
private: c.settings.private, private: c.settings.private,
peace: c.settings.peace, peace: c.settings.peace,
secureops: c.settings.secureops, secureops: c.settings.secureops,
securevoices: c.settings.securevoices,
keeptopic: c.settings.keeptopic, keeptopic: c.settings.keeptopic,
topiclock: c.settings.topiclock, topiclock: c.settings.topiclock,
topic: c.topic.clone(), topic: c.topic.clone(),

View file

@ -1538,6 +1538,17 @@ impl Engine {
} }
NetEvent::ChannelVoice { channel, uid, voice } => { NetEvent::ChannelVoice { channel, uid, voice } => {
self.network.set_voice(&channel, &uid, voice); self.network.set_voice(&channel, &uid, voice);
// SECUREVOICES: a user who gains +v without voice-level (or higher)
// access loses it. A services bot is staff, never a member.
let is_bot = self.bot_uids.values().any(|b| b == &uid);
if voice && !is_bot {
if let Some(c) = self.db.channel(&channel) {
if c.settings.securevoices && !self.network.account_of(&uid).is_some_and(|a| c.join_mode(a).is_some()) {
let from = self.chan_service.clone().unwrap_or_default();
return self.finish(out, vec![NetAction::ChannelMode { from, channel, modes: format!("-v {uid}") }]);
}
}
}
Vec::new() Vec::new()
} }
NetEvent::ChannelKey { channel, key } => { NetEvent::ChannelKey { channel, key } => {

View file

@ -5286,6 +5286,32 @@
assert!(out.iter().any(|a| matches!(a, NetAction::SetHost { uid, host } if uid == "000AAAAAB" && host == "cloak.host")), "logout restores the cloak: {out:?}"); assert!(out.iter().any(|a| matches!(a, NetAction::SetHost { uid, host } if uid == "000AAAAAB" && host == "cloak.host")), "logout restores the cloak: {out:?}");
} }
// SECUREVOICES strips voice from a user with no channel access, but leaves an
// access holder's voice alone.
#[test]
fn securevoices_strips_voice_from_non_access_users() {
use echo_chanserv::ChanServ;
let path = std::env::temp_dir().join("echo-securevoices.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 ns = NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 };
let cs = ChanServ { uid: "42SAAAAAB".into() };
let mut e = Engine::new(vec![Box::new(ns), Box::new(cs)], 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() });
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAB".into(), text: "SET #c SECUREVOICES ON".into() });
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into(), host: "h".into(), ip: "0.0.0.0".into() });
let out = e.handle(NetEvent::ChannelVoice { channel: "#c".into(), uid: "000AAAAAC".into(), voice: true });
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#c" && modes == "-v 000AAAAAC")), "non-access voice stripped: {out:?}");
let out = e.handle(NetEvent::ChannelVoice { channel: "#c".into(), uid: "000AAAAAB".into(), voice: true });
assert!(!out.iter().any(|a| matches!(a, NetAction::ChannelMode { modes, .. } if modes.starts_with("-v"))), "founder keeps voice: {out:?}");
}
// ChanServ SET: description and founder transfer, founder-gated. // ChanServ SET: description and founder transfer, founder-gated.
#[test] #[test]
fn chanserv_set() { fn chanserv_set() {

View file

@ -273,6 +273,7 @@ pub fn import_anope(anope_path: &str, out_path: &str, node: &str) -> std::io::Re
private: any(&["PRIVATE", "CS_PRIVATE"]), private: any(&["PRIVATE", "CS_PRIVATE"]),
peace: any(&["PEACE"]), peace: any(&["PEACE"]),
secureops: any(&["SECUREOPS", "CS_SECUREOPS"]), secureops: any(&["SECUREOPS", "CS_SECUREOPS"]),
securevoices: false, // no Anope equivalent
restricted: any(&["RESTRICTED", "CS_RESTRICTED"]), restricted: any(&["RESTRICTED", "CS_RESTRICTED"]),
noautoop: any(&["NOAUTOOP", "CS_NOAUTOOP"]), noautoop: any(&["NOAUTOOP", "CS_NOAUTOOP"]),
keeptopic: any(&["KEEPTOPIC"]), keeptopic: any(&["KEEPTOPIC"]),