Add SECUREVOICES: strip voice from users without channel access
All checks were successful
CI / check (push) Successful in 5m20s
All checks were successful
CI / check (push) Successful in 5m20s
This commit is contained in:
parent
b2f075ee00
commit
ae90388fbf
15 changed files with 62 additions and 8 deletions
|
|
@ -1849,6 +1849,7 @@ pub struct ChannelView {
|
|||
pub private: bool,
|
||||
pub peace: bool,
|
||||
pub secureops: bool,
|
||||
pub securevoices: bool,
|
||||
pub keeptopic: bool,
|
||||
pub topiclock: bool,
|
||||
pub suspended: bool,
|
||||
|
|
@ -1871,6 +1872,7 @@ pub enum ChanSetting {
|
|||
Private,
|
||||
Peace,
|
||||
SecureOps,
|
||||
SecureVoices,
|
||||
KeepTopic,
|
||||
TopicLock,
|
||||
// Kick users without channel access when they join.
|
||||
|
|
|
|||
|
|
@ -1368,5 +1368,6 @@
|
|||
"Confirm now": "Jetzt bestätigen",
|
||||
"… 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",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -1368,5 +1368,6 @@
|
|||
"Confirm now": "Confirmar ahora",
|
||||
"… 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",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -1368,5 +1368,6 @@
|
|||
"Confirm now": "Confirmar ahora",
|
||||
"… 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",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -1368,5 +1368,6 @@
|
|||
"Confirm now": "Confirmer maintenant",
|
||||
"… 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",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -1368,5 +1368,6 @@
|
|||
"Confirm now": "Confirmar agora",
|
||||
"… 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",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -1368,5 +1368,6 @@
|
|||
"Confirm now": "Confirmar agora",
|
||||
"… 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",
|
||||
"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."
|
||||
}
|
||||
|
|
@ -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: "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: "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: "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." },
|
||||
|
|
@ -201,6 +201,7 @@ impl Service for ChanServ {
|
|||
if info.private { opts.push("PRIVATE"); }
|
||||
if info.peace { opts.push("PEACE"); }
|
||||
if info.secureops { opts.push("SECUREOPS"); }
|
||||
if info.securevoices { opts.push("SECUREVOICES"); }
|
||||
if info.keeptopic { opts.push("KEEPTOPIC"); }
|
||||
if info.topiclock { opts.push("TOPICLOCK"); }
|
||||
if !opts.is_empty() {
|
||||
|
|
|
|||
|
|
@ -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("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("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("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("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::Peace => "PEACE",
|
||||
ChanSetting::SecureOps => "SECUREOPS",
|
||||
ChanSetting::SecureVoices => "SECUREVOICES",
|
||||
ChanSetting::Restricted => "RESTRICTED",
|
||||
ChanSetting::AutoOp => "AUTOOP",
|
||||
ChanSetting::KeepTopic => "KEEPTOPIC",
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ impl Db {
|
|||
ChanSetting::Private => settings.private = on,
|
||||
ChanSetting::Peace => settings.peace = on,
|
||||
ChanSetting::SecureOps => settings.secureops = on,
|
||||
ChanSetting::SecureVoices => settings.securevoices = on,
|
||||
ChanSetting::Restricted => settings.restricted = on,
|
||||
ChanSetting::AutoOp => settings.noautoop = !on, // stored inverted (default on)
|
||||
ChanSetting::KeepTopic => settings.keeptopic = on,
|
||||
|
|
|
|||
|
|
@ -428,6 +428,9 @@ pub struct ChanSettings {
|
|||
// Strip channel-operator status from anyone without op-level access.
|
||||
#[serde(default)]
|
||||
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.
|
||||
#[serde(default)]
|
||||
pub restricted: bool,
|
||||
|
|
|
|||
|
|
@ -653,6 +653,7 @@ fn channel_view(c: &ChannelInfo) -> ChannelView {
|
|||
private: c.settings.private,
|
||||
peace: c.settings.peace,
|
||||
secureops: c.settings.secureops,
|
||||
securevoices: c.settings.securevoices,
|
||||
keeptopic: c.settings.keeptopic,
|
||||
topiclock: c.settings.topiclock,
|
||||
topic: c.topic.clone(),
|
||||
|
|
|
|||
|
|
@ -1538,6 +1538,17 @@ impl Engine {
|
|||
}
|
||||
NetEvent::ChannelVoice { 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()
|
||||
}
|
||||
NetEvent::ChannelKey { channel, key } => {
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
|
||||
// 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.
|
||||
#[test]
|
||||
fn chanserv_set() {
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ pub fn import_anope(anope_path: &str, out_path: &str, node: &str) -> std::io::Re
|
|||
private: any(&["PRIVATE", "CS_PRIVATE"]),
|
||||
peace: any(&["PEACE"]),
|
||||
secureops: any(&["SECUREOPS", "CS_SECUREOPS"]),
|
||||
securevoices: false, // no Anope equivalent
|
||||
restricted: any(&["RESTRICTED", "CS_RESTRICTED"]),
|
||||
noautoop: any(&["NOAUTOOP", "CS_NOAUTOOP"]),
|
||||
keeptopic: any(&["KEEPTOPIC"]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue