Compare commits
No commits in common. "b500698e82ef61dc96a8b4966d0a0479053e4464" and "a2bf5ed5533c5b112a2e7b1e866dcddb8f769d13" have entirely different histories.
b500698e82
...
a2bf5ed553
17 changed files with 56 additions and 277 deletions
|
|
@ -2318,7 +2318,6 @@ pub trait NetView {
|
||||||
// A user's identity for extban / AKICK matching, if known.
|
// A user's identity for extban / AKICK matching, if known.
|
||||||
fn ban_target(&self, uid: &str) -> Option<BanTarget<'_>>;
|
fn ban_target(&self, uid: &str) -> Option<BanTarget<'_>>;
|
||||||
fn uids_logged_into(&self, account: &str) -> Vec<String>;
|
fn uids_logged_into(&self, account: &str) -> Vec<String>;
|
||||||
fn channels_of(&self, uid: &str) -> Vec<String>;
|
|
||||||
fn is_op(&self, channel: &str, uid: &str) -> bool;
|
fn is_op(&self, channel: &str, uid: &str) -> bool;
|
||||||
fn channel_members(&self, channel: &str) -> Vec<String>;
|
fn channel_members(&self, channel: &str) -> Vec<String>;
|
||||||
fn channel_key(&self, channel: &str) -> Option<&str>;
|
fn channel_key(&self, channel: &str) -> Option<&str>;
|
||||||
|
|
@ -2446,28 +2445,6 @@ pub fn next_guest_nick(base: &str, seq: &mut u32, net: &dyn NetView, store: &dyn
|
||||||
candidate
|
candidate
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tell `target` account they were affected by someone else's action: notice
|
|
||||||
/// every online session, or leave a memo (from `by`) if they're offline. The
|
|
||||||
/// message renders in the target's own language. No-op when the actor is the
|
|
||||||
/// target (self-service needs no notice).
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn notify_or_memo(ctx: &mut ServiceCtx, net: &dyn NetView, store: &mut dyn Store, service: &str, by: &str, target: &str, msgid: &str, args: &[(&str, String)]) {
|
|
||||||
// A `!group` target is not a memo-able account, and self-service needs no notice.
|
|
||||||
if target.starts_with('!') || by.eq_ignore_ascii_case(target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let lang = store.language_of(target).unwrap_or_else(|| store.default_language());
|
|
||||||
let text = render(&lang, msgid, args);
|
|
||||||
let online = net.uids_logged_into(target);
|
|
||||||
if online.is_empty() {
|
|
||||||
let _ = store.memo_send(target, by, &text, false);
|
|
||||||
} else {
|
|
||||||
for uid in online {
|
|
||||||
ctx.notice(service, &uid, text.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject a look-alike / mixed-script registration name (nick or channel): the
|
// Reject a look-alike / mixed-script registration name (nick or channel): the
|
||||||
// impersonation trick that a message-content filter never sees at the registration
|
// impersonation trick that a message-content filter never sees at the registration
|
||||||
// seam. Returns a rejection reason, or None if the name is fine. Genuine
|
// seam. Returns a rejection reason, or None if the name is fine. Genuine
|
||||||
|
|
|
||||||
12
lang/de.json
12
lang/de.json
|
|
@ -1353,15 +1353,5 @@
|
||||||
"modulo by zero": "Modulo durch null",
|
"modulo by zero": "Modulo durch null",
|
||||||
"that expression rolls too many dice": "dieser Ausdruck würfelt zu viele Würfel",
|
"that expression rolls too many dice": "dieser Ausdruck würfelt zu viele Würfel",
|
||||||
"hypot() takes two arguments": "hypot() nimmt zwei Argumente",
|
"hypot() takes two arguments": "hypot() nimmt zwei Argumente",
|
||||||
"pow() takes two arguments": "pow() nimmt zwei Argumente",
|
"pow() takes two arguments": "pow() nimmt zwei Argumente"
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002 as \u0002{level}\u0002.": "\u0002{by}\u0002 hat dich zu \u0002{chan}\u0002 als \u0002{level}\u0002 hinzugefügt.",
|
|
||||||
"\u0002{by}\u0002 removed your access to \u0002{chan}\u0002.": "\u0002{by}\u0002 hat deinen Zugang zu \u0002{chan}\u0002 entfernt.",
|
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 hat dich zur {word}-Liste von \u0002{chan}\u0002 hinzugefügt.",
|
|
||||||
"\u0002{by}\u0002 removed you from \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 hat dich von der {word}-Liste von \u0002{chan}\u0002 entfernt.",
|
|
||||||
"\u0002{by}\u0002 set your access on \u0002{chan}\u0002 to \u0002{letters}\u0002.": "\u0002{by}\u0002 hat deinen Zugang in \u0002{chan}\u0002 auf \u0002{letters}\u0002 gesetzt.",
|
|
||||||
"\u0002{by}\u0002 transferred the \u0002{chan}\u0002 founder to you.": "\u0002{by}\u0002 hat dir die Gründerrolle von \u0002{chan}\u0002 übertragen.",
|
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 hat dich als Nachfolger von \u0002{chan}\u0002 festgelegt.",
|
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 Kanal zu deiner Auto-Join-Liste hinzugefügt.",
|
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 Kanäle zu deiner Auto-Join-Liste hinzugefügt.",
|
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Unbekannter AJOIN-Befehl \u0002{other}\u0002. Nutze \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 oder \u0002LIST\u0002."
|
|
||||||
}
|
}
|
||||||
|
|
@ -1353,15 +1353,5 @@
|
||||||
"modulo by zero": "módulo por cero",
|
"modulo by zero": "módulo por cero",
|
||||||
"that expression rolls too many dice": "esa expresión tira demasiados dados",
|
"that expression rolls too many dice": "esa expresión tira demasiados dados",
|
||||||
"hypot() takes two arguments": "hypot() toma dos argumentos",
|
"hypot() takes two arguments": "hypot() toma dos argumentos",
|
||||||
"pow() takes two arguments": "pow() toma dos argumentos",
|
"pow() takes two arguments": "pow() toma dos argumentos"
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002 as \u0002{level}\u0002.": "\u0002{by}\u0002 te agregó a \u0002{chan}\u0002 como \u0002{level}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed your access to \u0002{chan}\u0002.": "\u0002{by}\u0002 te sacó el acceso a \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 te agregó a la lista {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed you from \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 te sacó de la lista {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set your access on \u0002{chan}\u0002 to \u0002{letters}\u0002.": "\u0002{by}\u0002 estableció tu acceso en \u0002{chan}\u0002 a \u0002{letters}\u0002.",
|
|
||||||
"\u0002{by}\u0002 transferred the \u0002{chan}\u0002 founder to you.": "\u0002{by}\u0002 te transfirió el estatus de fundador de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 te estableció como sucesor de \u0002{chan}\u0002.",
|
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal agregado a tu lista de auto-unión.",
|
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canales agregados a tu lista de auto-unión.",
|
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconocido \u0002{other}\u0002. Usá \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 o \u0002LIST\u0002."
|
|
||||||
}
|
}
|
||||||
12
lang/es.json
12
lang/es.json
|
|
@ -1353,15 +1353,5 @@
|
||||||
"modulo by zero": "módulo por cero",
|
"modulo by zero": "módulo por cero",
|
||||||
"that expression rolls too many dice": "esa expresión lanza demasiados dados",
|
"that expression rolls too many dice": "esa expresión lanza demasiados dados",
|
||||||
"hypot() takes two arguments": "hypot() toma dos argumentos",
|
"hypot() takes two arguments": "hypot() toma dos argumentos",
|
||||||
"pow() takes two arguments": "pow() toma dos argumentos",
|
"pow() takes two arguments": "pow() toma dos argumentos"
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002 as \u0002{level}\u0002.": "\u0002{by}\u0002 te añadió a \u0002{chan}\u0002 como \u0002{level}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed your access to \u0002{chan}\u0002.": "\u0002{by}\u0002 te quitó el acceso a \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 te añadió a la lista {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed you from \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 te quitó de la lista {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set your access on \u0002{chan}\u0002 to \u0002{letters}\u0002.": "\u0002{by}\u0002 estableció tu acceso en \u0002{chan}\u0002 a \u0002{letters}\u0002.",
|
|
||||||
"\u0002{by}\u0002 transferred the \u0002{chan}\u0002 founder to you.": "\u0002{by}\u0002 te transfirió el estatus de fundador de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 te estableció como sucesor de \u0002{chan}\u0002.",
|
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal añadido a tu lista de auto-unión.",
|
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canales añadidos a tu lista de auto-unión.",
|
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconocido \u0002{other}\u0002. Usa \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 o \u0002LIST\u0002."
|
|
||||||
}
|
}
|
||||||
12
lang/fr.json
12
lang/fr.json
|
|
@ -1353,15 +1353,5 @@
|
||||||
"modulo by zero": "modulo par zéro",
|
"modulo by zero": "modulo par zéro",
|
||||||
"that expression rolls too many dice": "cette expression lance trop de dés",
|
"that expression rolls too many dice": "cette expression lance trop de dés",
|
||||||
"hypot() takes two arguments": "hypot() prend deux arguments",
|
"hypot() takes two arguments": "hypot() prend deux arguments",
|
||||||
"pow() takes two arguments": "pow() prend deux arguments",
|
"pow() takes two arguments": "pow() prend deux arguments"
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002 as \u0002{level}\u0002.": "\u0002{by}\u0002 vous a ajouté à \u0002{chan}\u0002 en tant que \u0002{level}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed your access to \u0002{chan}\u0002.": "\u0002{by}\u0002 vous a retiré l'accès à \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 vous a ajouté à la liste {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed you from \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 vous a retiré de la liste {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set your access on \u0002{chan}\u0002 to \u0002{letters}\u0002.": "\u0002{by}\u0002 a défini votre accès sur \u0002{chan}\u0002 à \u0002{letters}\u0002.",
|
|
||||||
"\u0002{by}\u0002 transferred the \u0002{chan}\u0002 founder to you.": "\u0002{by}\u0002 vous a transféré le statut de fondateur de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 vous a défini comme successeur de \u0002{chan}\u0002.",
|
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 salon ajouté à votre liste d'auto-join.",
|
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 salons ajoutés à votre liste d'auto-join.",
|
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Commande AJOIN inconnue \u0002{other}\u0002. Utilisez \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002."
|
|
||||||
}
|
}
|
||||||
|
|
@ -1353,15 +1353,5 @@
|
||||||
"modulo by zero": "módulo por zero",
|
"modulo by zero": "módulo por zero",
|
||||||
"that expression rolls too many dice": "essa expressão rola dados demais",
|
"that expression rolls too many dice": "essa expressão rola dados demais",
|
||||||
"hypot() takes two arguments": "hypot() recebe dois argumentos",
|
"hypot() takes two arguments": "hypot() recebe dois argumentos",
|
||||||
"pow() takes two arguments": "pow() recebe dois argumentos",
|
"pow() takes two arguments": "pow() recebe dois argumentos"
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002 as \u0002{level}\u0002.": "\u0002{by}\u0002 adicionou você a \u0002{chan}\u0002 como \u0002{level}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed your access to \u0002{chan}\u0002.": "\u0002{by}\u0002 removeu seu acesso a \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 adicionou você à lista {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed you from \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 removeu você da lista {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set your access on \u0002{chan}\u0002 to \u0002{letters}\u0002.": "\u0002{by}\u0002 definiu seu acesso em \u0002{chan}\u0002 como \u0002{letters}\u0002.",
|
|
||||||
"\u0002{by}\u0002 transferred the \u0002{chan}\u0002 founder to you.": "\u0002{by}\u0002 transferiu a fundação de \u0002{chan}\u0002 para você.",
|
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 definiu você como sucessor de \u0002{chan}\u0002.",
|
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal adicionado à sua lista de auto-entrada.",
|
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canais adicionados à sua lista de auto-entrada.",
|
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconhecido \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002."
|
|
||||||
}
|
}
|
||||||
12
lang/pt.json
12
lang/pt.json
|
|
@ -1353,15 +1353,5 @@
|
||||||
"modulo by zero": "módulo por zero",
|
"modulo by zero": "módulo por zero",
|
||||||
"that expression rolls too many dice": "essa expressão lança demasiados dados",
|
"that expression rolls too many dice": "essa expressão lança demasiados dados",
|
||||||
"hypot() takes two arguments": "hypot() recebe dois argumentos",
|
"hypot() takes two arguments": "hypot() recebe dois argumentos",
|
||||||
"pow() takes two arguments": "pow() recebe dois argumentos",
|
"pow() takes two arguments": "pow() recebe dois argumentos"
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002 as \u0002{level}\u0002.": "\u0002{by}\u0002 adicionou-o a \u0002{chan}\u0002 como \u0002{level}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed your access to \u0002{chan}\u0002.": "\u0002{by}\u0002 removeu o seu acesso a \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 added you to \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 adicionou-o à lista {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 removed you from \u0002{chan}\u0002's {word} list.": "\u0002{by}\u0002 removeu-o da lista {word} de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set your access on \u0002{chan}\u0002 to \u0002{letters}\u0002.": "\u0002{by}\u0002 definiu o seu acesso em \u0002{chan}\u0002 como \u0002{letters}\u0002.",
|
|
||||||
"\u0002{by}\u0002 transferred the \u0002{chan}\u0002 founder to you.": "\u0002{by}\u0002 transferiu-lhe a fundação de \u0002{chan}\u0002.",
|
|
||||||
"\u0002{by}\u0002 set you as successor of \u0002{chan}\u0002.": "\u0002{by}\u0002 definiu-o como sucessor de \u0002{chan}\u0002.",
|
|
||||||
"Added \u0002{count}\u0002 channel to your auto-join list.": "\u0002{count}\u0002 canal adicionado à sua lista de auto-entrada.",
|
|
||||||
"Added \u0002{count}\u0002 channels to your auto-join list.": "\u0002{count}\u0002 canais adicionados à sua lista de auto-entrada.",
|
|
||||||
"Unknown AJOIN command \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 or \u0002LIST\u0002.": "Comando AJOIN desconhecido \u0002{other}\u0002. Use \u0002ADD\u0002, \u0002ADDALL\u0002, \u0002DEL\u0002 ou \u0002LIST\u0002."
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use echo_api::{NetView, Sender, ServiceCtx, Store};
|
use echo_api::Store;
|
||||||
|
use echo_api::{Sender, ServiceCtx};
|
||||||
use echo_api::t;
|
use echo_api::t;
|
||||||
|
|
||||||
// The named tiers ACCESS ADD accepts, high to low; the granular FLAGS command
|
// The named tiers ACCESS ADD accepts, high to low; the granular FLAGS command
|
||||||
|
|
@ -6,7 +7,7 @@ use echo_api::t;
|
||||||
const TIERS: [&str; 4] = ["sop", "op", "halfop", "voice"];
|
const TIERS: [&str; 4] = ["sop", "op", "halfop", "voice"];
|
||||||
|
|
||||||
// ACCESS <#channel> LIST | ADD <account> <sop|op|halfop|voice> | DEL <account>
|
// ACCESS <#channel> LIST | ADD <account> <sop|op|halfop|voice> | DEL <account>
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
let Some(&chan) = args.get(1) else {
|
let Some(&chan) = args.get(1) else {
|
||||||
ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD <account|!group> <sop|op|halfop|voice> | DEL <account|!group>");
|
ctx.notice(me, from.uid, "Syntax: ACCESS <#channel> LIST | ADD <account|!group> <sop|op|halfop|voice> | DEL <account|!group>");
|
||||||
return;
|
return;
|
||||||
|
|
@ -36,11 +37,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match db.access_add(chan, account, &level) {
|
match db.access_add(chan, account, &level) {
|
||||||
Ok(()) => {
|
Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Added \x02{account}\x02 to \x02{chan}\x02 as \x02{level}\x02.", account = account, chan = chan, level = level)),
|
||||||
ctx.notice(me, from.uid, t!(ctx, "Added \x02{account}\x02 to \x02{chan}\x02 as \x02{level}\x02.", account = account, chan = chan, level = level));
|
|
||||||
let by = from.account.unwrap_or(from.nick);
|
|
||||||
echo_api::notify_or_memo(ctx, net, db, me, by, account, "\x02{by}\x02 added you to \x02{chan}\x02 as \x02{level}\x02.", &[("by", by.to_string()), ("chan", chan.to_string()), ("level", level.clone())]);
|
|
||||||
}
|
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,11 +50,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match db.access_del(chan, account) {
|
match db.access_del(chan, account) {
|
||||||
Ok(true) => {
|
Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Removed \x02{account}\x02 from \x02{chan}\x02.", account = account, chan = chan)),
|
||||||
ctx.notice(me, from.uid, t!(ctx, "Removed \x02{account}\x02 from \x02{chan}\x02.", account = account, chan = chan));
|
|
||||||
let by = from.account.unwrap_or(from.nick);
|
|
||||||
echo_api::notify_or_memo(ctx, net, db, me, by, account, "\x02{by}\x02 removed your access to \x02{chan}\x02.", &[("by", by.to_string()), ("chan", chan.to_string())]);
|
|
||||||
}
|
|
||||||
Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 has no access to \x02{chan}\x02.", account = account, chan = chan)),
|
Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 has no access to \x02{chan}\x02.", account = account, chan = chan)),
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use echo_api::{Flags, NetView, Sender, ServiceCtx, Store, ACCESS_FLAGS};
|
use echo_api::{Flags, Sender, ServiceCtx, Store, ACCESS_FLAGS};
|
||||||
use echo_api::t;
|
use echo_api::t;
|
||||||
|
|
||||||
// FLAGS <#channel> [account [+/-flags]]: the granular access model. With no
|
// FLAGS <#channel> [account [+/-flags]]: the granular access model. With no
|
||||||
|
|
@ -7,7 +7,7 @@ use echo_api::t;
|
||||||
// t topic i invite a access-list s settings g greet. Viewing needs op access;
|
// t topic i invite a access-list s settings g greet. Viewing needs op access;
|
||||||
// changing needs the founder or the \x02a\x02 flag. Every stored level — a tier
|
// changing needs the founder or the \x02a\x02 flag. Every stored level — a tier
|
||||||
// preset ("op"/"sop"/…) or a raw flag string — resolves through `Flags`.
|
// preset ("op"/"sop"/…) or a raw flag string — resolves through `Flags`.
|
||||||
pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
let Some(info) = db.channel(chan) else {
|
let Some(info) = db.channel(chan) else {
|
||||||
ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
|
ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
|
||||||
return;
|
return;
|
||||||
|
|
@ -76,11 +76,7 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv
|
||||||
}
|
}
|
||||||
let letters = updated.to_letters();
|
let letters = updated.to_letters();
|
||||||
match db.access_add(chan, target, &letters) {
|
match db.access_add(chan, target, &letters) {
|
||||||
Ok(()) => {
|
Ok(()) => ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 on \x02{chan}\x02 now holds \x02{letters}\x02.", target = target, chan = chan, letters = letters)),
|
||||||
ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 on \x02{chan}\x02 now holds \x02{letters}\x02.", target = target, chan = chan, letters = letters));
|
|
||||||
let by = from.account.unwrap_or(from.nick);
|
|
||||||
echo_api::notify_or_memo(ctx, net, db, me, by, target, "\x02{by}\x02 set your access on \x02{chan}\x02 to \x02{letters}\x02.", &[("by", by.to_string()), ("chan", chan.to_string()), ("letters", letters.clone())]);
|
|
||||||
}
|
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,13 +288,13 @@ impl Service for ChanServ {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("MODE") => mode::handle(me, from, args, ctx, db),
|
Some("MODE") => mode::handle(me, from, args, ctx, db),
|
||||||
Some("ACCESS") => access::handle(me, from, args, ctx, net, db),
|
Some("ACCESS") => access::handle(me, from, args, ctx, db),
|
||||||
Some("FLAGS") => {
|
Some("FLAGS") => {
|
||||||
let Some(&chan) = args.get(1) else {
|
let Some(&chan) = args.get(1) else {
|
||||||
ctx.notice(me, from.uid, "Syntax: FLAGS <#channel> [account [+/-flags]]");
|
ctx.notice(me, from.uid, "Syntax: FLAGS <#channel> [account [+/-flags]]");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
flags::handle(me, from, chan, args, ctx, net, db);
|
flags::handle(me, from, chan, args, ctx, db);
|
||||||
}
|
}
|
||||||
Some("OP") => op::handle(me, from, "+o", args, ctx, net, db),
|
Some("OP") => op::handle(me, from, "+o", args, ctx, net, db),
|
||||||
Some("DEOP") => op::handle(me, from, "-o", args, ctx, net, db),
|
Some("DEOP") => op::handle(me, from, "-o", args, ctx, net, db),
|
||||||
|
|
@ -320,7 +320,7 @@ impl Service for ChanServ {
|
||||||
Some("UNSUSPEND") => suspend::handle(me, from, args, ctx, net, db, false),
|
Some("UNSUSPEND") => suspend::handle(me, from, args, ctx, net, db, false),
|
||||||
Some("NOEXPIRE") => noexpire::handle(me, from, args, ctx, db),
|
Some("NOEXPIRE") => noexpire::handle(me, from, args, ctx, db),
|
||||||
Some("LIST") => list::handle(me, from, args, ctx, db),
|
Some("LIST") => list::handle(me, from, args, ctx, db),
|
||||||
Some("SET") => set::handle(me, from, args, ctx, net, db),
|
Some("SET") => set::handle(me, from, args, ctx, db),
|
||||||
Some("ENTRYMSG") => entrymsg::handle(me, from, args, ctx, db),
|
Some("ENTRYMSG") => entrymsg::handle(me, from, args, ctx, db),
|
||||||
Some("GETKEY") => getkey::handle(me, from, args, ctx, net, db),
|
Some("GETKEY") => getkey::handle(me, from, args, ctx, net, db),
|
||||||
Some("SEEN") => seen::handle(me, from, args, ctx, net),
|
Some("SEEN") => seen::handle(me, from, args, ctx, net),
|
||||||
|
|
@ -328,10 +328,10 @@ impl Service for ChanServ {
|
||||||
// the mode lock and akick list, so SYNC is the same handler.
|
// the mode lock and akick list, so SYNC is the same handler.
|
||||||
Some("ENFORCE") | Some("SYNC") => enforce::handle(me, from, args, ctx, net, db),
|
Some("ENFORCE") | Some("SYNC") => enforce::handle(me, from, args, ctx, net, db),
|
||||||
Some("CLONE") => clone::handle(me, from, args, ctx, db),
|
Some("CLONE") => clone::handle(me, from, args, ctx, db),
|
||||||
Some("SOP") => xop::handle(me, from, "SOP", "sop", args, ctx, net, db),
|
Some("SOP") => xop::handle(me, from, "SOP", "sop", args, ctx, db),
|
||||||
Some("AOP") => xop::handle(me, from, "AOP", "op", args, ctx, net, db),
|
Some("AOP") => xop::handle(me, from, "AOP", "op", args, ctx, db),
|
||||||
Some("HOP") => xop::handle(me, from, "HOP", "halfop", args, ctx, net, db),
|
Some("HOP") => xop::handle(me, from, "HOP", "halfop", args, ctx, db),
|
||||||
Some("VOP") => xop::handle(me, from, "VOP", "voice", args, ctx, net, db),
|
Some("VOP") => xop::handle(me, from, "VOP", "voice", args, ctx, db),
|
||||||
Some("HELP") => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()),
|
Some("HELP") => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()),
|
||||||
// Direct `/msg ChanServ FOO` earns this reply; the fantasy router
|
// Direct `/msg ChanServ FOO` earns this reply; the fantasy router
|
||||||
// gates on COMMANDS first so an in-channel `!foo` stays silent.
|
// gates on COMMANDS first so an in-channel `!foo` stays silent.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use echo_api::{ChanSetting, NetView, Sender, ServiceCtx, Store};
|
use echo_api::{ChanSetting, Sender, ServiceCtx, Store};
|
||||||
use echo_api::t;
|
use echo_api::t;
|
||||||
|
|
||||||
// SET <#channel> FOUNDER <account> | DESC <text>: founder-only channel settings.
|
// SET <#channel> FOUNDER <account> | DESC <text>: founder-only channel settings.
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
let Some(&chan) = args.get(1) else {
|
let Some(&chan) = args.get(1) else {
|
||||||
ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | DESC <text>");
|
ctx.notice(me, from.uid, "Syntax: SET <#channel> FOUNDER <account> | DESC <text>");
|
||||||
return;
|
return;
|
||||||
|
|
@ -32,11 +32,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match db.set_founder(chan, account) {
|
match db.set_founder(chan, account) {
|
||||||
Ok(()) => {
|
Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Founder of \x02{chan}\x02 transferred to \x02{account}\x02.", chan = chan, account = account)),
|
||||||
ctx.notice(me, from.uid, t!(ctx, "Founder of \x02{chan}\x02 transferred to \x02{account}\x02.", chan = chan, account = account));
|
|
||||||
let by = from.account.unwrap_or(from.nick);
|
|
||||||
echo_api::notify_or_memo(ctx, net, db, me, by, account, "\x02{by}\x02 transferred the \x02{chan}\x02 founder to you.", &[("by", by.to_string()), ("chan", chan.to_string())]);
|
|
||||||
}
|
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,11 +48,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match db.set_successor(chan, Some(acct)) {
|
match db.set_successor(chan, Some(acct)) {
|
||||||
Ok(()) => {
|
Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Successor of \x02{chan}\x02 set to \x02{acct}\x02. They inherit it if your account is dropped or expires.", chan = chan, acct = acct)),
|
||||||
ctx.notice(me, from.uid, t!(ctx, "Successor of \x02{chan}\x02 set to \x02{acct}\x02. They inherit it if your account is dropped or expires.", chan = chan, acct = acct));
|
|
||||||
let by = from.account.unwrap_or(from.nick);
|
|
||||||
echo_api::notify_or_memo(ctx, net, db, me, by, acct, "\x02{by}\x02 set you as successor of \x02{chan}\x02.", &[("by", by.to_string()), ("chan", chan.to_string())]);
|
|
||||||
}
|
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
use echo_api::{access_role, NetView, Sender, ServiceCtx, Store};
|
use echo_api::{access_role, Sender, ServiceCtx, Store};
|
||||||
use echo_api::t;
|
use echo_api::t;
|
||||||
|
|
||||||
// SOP/AOP/HOP/VOP <#channel> ADD <account> | DEL <account> | LIST — tiered
|
// SOP/AOP/HOP/VOP <#channel> ADD <account> | DEL <account> | LIST — tiered
|
||||||
// shortcuts over the access list. `level` is the tier they map to ("sop", "op",
|
// shortcuts over the access list. `level` is the tier they map to ("sop", "op",
|
||||||
// "halfop", "voice"); `word` is the tier the user typed ("SOP".."VOP").
|
// "halfop", "voice"); `word` is the tier the user typed ("SOP".."VOP").
|
||||||
#[allow(clippy::too_many_arguments)]
|
pub fn handle(me: &str, from: &Sender, word: &str, level: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
pub fn handle(me: &str, from: &Sender, word: &str, level: &str, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) {
|
|
||||||
let Some(&chan) = args.get(1) else {
|
let Some(&chan) = args.get(1) else {
|
||||||
ctx.notice(me, from.uid, t!(ctx, "Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST", word = word));
|
ctx.notice(me, from.uid, t!(ctx, "Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST", word = word));
|
||||||
return;
|
return;
|
||||||
|
|
@ -31,11 +30,7 @@ pub fn handle(me: &str, from: &Sender, word: &str, level: &str, args: &[&str], c
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match db.access_add(chan, account, level) {
|
match db.access_add(chan, account, level) {
|
||||||
Ok(()) => {
|
Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Added \x02{account}\x02 to \x02{chan}\x02's {word} list.", account = account, chan = chan, word = word)),
|
||||||
ctx.notice(me, from.uid, t!(ctx, "Added \x02{account}\x02 to \x02{chan}\x02's {word} list.", account = account, chan = chan, word = word));
|
|
||||||
let by = from.account.unwrap_or(from.nick);
|
|
||||||
echo_api::notify_or_memo(ctx, net, db, me, by, account, "\x02{by}\x02 added you to \x02{chan}\x02's {word} list.", &[("by", by.to_string()), ("chan", chan.to_string()), ("word", word.to_string())]);
|
|
||||||
}
|
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,11 +43,7 @@ pub fn handle(me: &str, from: &Sender, word: &str, level: &str, args: &[&str], c
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match db.access_del(chan, account) {
|
match db.access_del(chan, account) {
|
||||||
Ok(true) => {
|
Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Removed \x02{account}\x02 from \x02{chan}\x02's {word} list.", account = account, chan = chan, word = word)),
|
||||||
ctx.notice(me, from.uid, t!(ctx, "Removed \x02{account}\x02 from \x02{chan}\x02's {word} list.", account = account, chan = chan, word = word));
|
|
||||||
let by = from.account.unwrap_or(from.nick);
|
|
||||||
echo_api::notify_or_memo(ctx, net, db, me, by, account, "\x02{by}\x02 removed you from \x02{chan}\x02's {word} list.", &[("by", by.to_string()), ("chan", chan.to_string()), ("word", word.to_string())]);
|
|
||||||
}
|
|
||||||
Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 has no access to \x02{chan}\x02.", account = account, chan = chan)),
|
Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 has no access to \x02{chan}\x02.", account = account, chan = chan)),
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use echo_api::{NetView, Sender, ServiceCtx, Store};
|
use echo_api::Store;
|
||||||
|
use echo_api::{Sender, ServiceCtx};
|
||||||
use echo_api::t;
|
use echo_api::t;
|
||||||
|
|
||||||
// A sane cap so a runaway list can't bloat an account or flood a user on identify.
|
// A sane cap so a runaway list can't bloat an account or flood a user on identify.
|
||||||
|
|
@ -6,7 +7,7 @@ const MAX_AJOIN: usize = 25;
|
||||||
|
|
||||||
// AJOIN [ADD <#channel> [key] | DEL <#channel> | LIST]: manage your auto-join
|
// AJOIN [ADD <#channel> [key] | DEL <#channel> | LIST]: manage your auto-join
|
||||||
// list — the channels NickServ joins you to each time you identify.
|
// list — the channels NickServ joins you to each time you identify.
|
||||||
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) {
|
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
|
||||||
let Some(account) = from.account else {
|
let Some(account) = from.account else {
|
||||||
ctx.notice(me, from.uid, "You must identify to NickServ to use \x02AJOIN\x02.");
|
ctx.notice(me, from.uid, "You must identify to NickServ to use \x02AJOIN\x02.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -32,26 +33,6 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
||||||
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("ADDALL") => {
|
|
||||||
let existing: std::collections::HashSet<String> =
|
|
||||||
db.ajoin_list(account).into_iter().map(|e| e.channel.to_ascii_lowercase()).collect();
|
|
||||||
let mut count = existing.len();
|
|
||||||
let mut added = 0u64;
|
|
||||||
for channel in net.channels_of(from.uid) {
|
|
||||||
if count >= MAX_AJOIN {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if existing.contains(&channel.to_ascii_lowercase()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let key = net.channel_key(&channel).unwrap_or("");
|
|
||||||
if let Ok(true) = db.ajoin_add(account, &channel, key) {
|
|
||||||
added += 1;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.notice(me, from.uid, echo_api::plural!(ctx, added, one = "Added \x02{count}\x02 channel to your auto-join list.", other = "Added \x02{count}\x02 channels to your auto-join list.", count = added));
|
|
||||||
}
|
|
||||||
Some("DEL") => {
|
Some("DEL") => {
|
||||||
let Some(&channel) = args.get(2) else {
|
let Some(&channel) = args.get(2) else {
|
||||||
ctx.notice(me, from.uid, "Syntax: AJOIN DEL <#channel>");
|
ctx.notice(me, from.uid, "Syntax: AJOIN DEL <#channel>");
|
||||||
|
|
@ -78,6 +59,6 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(other) => ctx.notice(me, from.uid, t!(ctx, "Unknown AJOIN command \x02{other}\x02. Use \x02ADD\x02, \x02ADDALL\x02, \x02DEL\x02 or \x02LIST\x02.", other = other)),
|
Some(other) => ctx.notice(me, from.uid, t!(ctx, "Unknown AJOIN command \x02{other}\x02. Use \x02ADD\x02, \x02DEL\x02 or \x02LIST\x02.", other = other)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ impl Service for NickServ {
|
||||||
Some("RECOVER") => ghost::handle(me, &self.guest_nick, &mut self.guest_seq, from, args, ctx, net, db, true),
|
Some("RECOVER") => ghost::handle(me, &self.guest_nick, &mut self.guest_seq, from, args, ctx, net, db, true),
|
||||||
Some("RESETPASS") => resetpass::handle(me, from, args, ctx, db),
|
Some("RESETPASS") => resetpass::handle(me, from, args, ctx, db),
|
||||||
Some("CONFIRM") => confirm::handle(me, from, args, ctx, db),
|
Some("CONFIRM") => confirm::handle(me, from, args, ctx, db),
|
||||||
Some("AJOIN") => ajoin::handle(me, from, args, ctx, net, db),
|
Some("AJOIN") => ajoin::handle(me, from, args, ctx, db),
|
||||||
Some("SUSPEND") => suspend::handle(me, from, args, ctx, net, db, true),
|
Some("SUSPEND") => suspend::handle(me, from, args, ctx, net, db, true),
|
||||||
Some("UNSUSPEND") => suspend::handle(me, from, args, ctx, net, db, false),
|
Some("UNSUSPEND") => suspend::handle(me, from, args, ctx, net, db, false),
|
||||||
Some("NOEXPIRE") => noexpire::handle(me, from, args, ctx, db),
|
Some("NOEXPIRE") => noexpire::handle(me, from, args, ctx, db),
|
||||||
|
|
|
||||||
|
|
@ -1381,7 +1381,6 @@ impl Engine {
|
||||||
self.network.user_nick_change(&uid, nick);
|
self.network.user_nick_change(&uid, nick);
|
||||||
self.pending_enforce.retain(|p| p.uid != uid); // they left the old nick
|
self.pending_enforce.retain(|p| p.uid != uid); // they left the old nick
|
||||||
let mut out = self.enforce_registered_nick(&uid, &new_nick);
|
let mut out = self.enforce_registered_nick(&uid, &new_nick);
|
||||||
out.extend(self.enforce_akick_on_nick(&uid));
|
|
||||||
if let Some(line) = self.notify_line('n', &uid, None, &format!("changed nick (was {old_nick})")) {
|
if let Some(line) = self.notify_line('n', &uid, None, &format!("changed nick (was {old_nick})")) {
|
||||||
out.push(line);
|
out.push(line);
|
||||||
}
|
}
|
||||||
|
|
@ -1478,11 +1477,28 @@ impl Engine {
|
||||||
}
|
}
|
||||||
// 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 => {
|
||||||
let hit = self.akick_hit(&uid, &channel);
|
// Match the akick list against the user's full identity —
|
||||||
if !hit.is_empty() {
|
// host and the extbans (account/realname/…) the ircd gave us.
|
||||||
acts.extend(hit);
|
let akick = {
|
||||||
} else if let Some(msg) = entrymsg {
|
let target = self.network.ban_target(&uid);
|
||||||
acts.push(NetAction::Notice { from, to: uid, text: msg });
|
target.as_ref().and_then(|t| {
|
||||||
|
self.db.channel(&channel).and_then(|c| c.akick_match(t)).map(|k| {
|
||||||
|
let reason = if k.reason.is_empty() { "You are banned from this channel.".to_string() } else { k.reason.clone() };
|
||||||
|
(k.mask.clone(), reason)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
match akick {
|
||||||
|
Some((mask, reason)) => {
|
||||||
|
self.network.channel_part(&channel, &uid);
|
||||||
|
acts.push(NetAction::ChannelMode { from: from.clone(), channel: channel.clone(), modes: format!("+b {mask}") });
|
||||||
|
acts.push(NetAction::Kick { from, channel, uid, reason });
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if let Some(msg) = entrymsg {
|
||||||
|
acts.push(NetAction::Notice { from, to: uid, text: msg });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1809,49 +1825,6 @@ impl Engine {
|
||||||
Some(NetAction::Privmsg { from: botuid, to: channel.to_string(), text: format!("[{acc}] {greet}") })
|
Some(NetAction::Privmsg { from: botuid, to: channel.to_string(), text: format!("[{acc}] {greet}") })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ban+kick `uid` from `channel` if their current identity matches its akick
|
|
||||||
// list. Parts them locally and returns the +b/KICK; empty when no match.
|
|
||||||
fn akick_hit(&mut self, uid: &str, channel: &str) -> Vec<NetAction> {
|
|
||||||
let matched = {
|
|
||||||
let target = self.network.ban_target(uid);
|
|
||||||
target.as_ref().and_then(|t| {
|
|
||||||
self.db.channel(channel).and_then(|c| c.akick_match(t)).map(|k| {
|
|
||||||
let reason = if k.reason.is_empty() { "You are banned from this channel.".to_string() } else { k.reason.clone() };
|
|
||||||
(k.mask.clone(), reason)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
|
||||||
match matched {
|
|
||||||
Some((mask, reason)) => {
|
|
||||||
let from = self.channel_mode_source(channel);
|
|
||||||
self.network.channel_part(channel, uid);
|
|
||||||
vec![
|
|
||||||
NetAction::ChannelMode { from: from.clone(), channel: channel.to_string(), modes: format!("+b {mask}") },
|
|
||||||
NetAction::Kick { from, channel: channel.to_string(), uid: uid.to_string(), reason },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
None => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A nick change rewrites the user's n!u@h, so re-run the akick check the join
|
|
||||||
// path does — otherwise a user joins clean, switches to a banned nick, and stays.
|
|
||||||
// Access holders are exempt, as on join.
|
|
||||||
fn enforce_akick_on_nick(&mut self, uid: &str) -> Vec<NetAction> {
|
|
||||||
let mut acts = Vec::new();
|
|
||||||
let account = self.network.account_of(uid).map(str::to_string);
|
|
||||||
for channel in self.network.channels_of(uid) {
|
|
||||||
if self.db.is_channel_suspended(&channel) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if account.as_deref().and_then(|a| self.db.channel_join_mode(&channel, a)).is_some() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
acts.extend(self.akick_hit(uid, &channel));
|
|
||||||
}
|
|
||||||
acts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fantasy commands: `!op nick`, `!kick nick`, `!topic …` spoken in a channel
|
// Fantasy commands: `!op nick`, `!kick nick`, `!topic …` spoken in a channel
|
||||||
// that has a bot assigned. We reuse ChanServ's real command handlers — the
|
// that has a bot assigned. We reuse ChanServ's real command handlers — the
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -615,9 +615,6 @@ impl NetView for Network {
|
||||||
fn uids_logged_into(&self, account: &str) -> Vec<String> {
|
fn uids_logged_into(&self, account: &str) -> Vec<String> {
|
||||||
Network::uids_logged_into(self, account)
|
Network::uids_logged_into(self, account)
|
||||||
}
|
}
|
||||||
fn channels_of(&self, uid: &str) -> Vec<String> {
|
|
||||||
Network::channels_of(self, uid)
|
|
||||||
}
|
|
||||||
fn is_op(&self, channel: &str, uid: &str) -> bool {
|
fn is_op(&self, channel: &str, uid: &str) -> bool {
|
||||||
Network::is_op(self, channel, uid)
|
Network::is_op(self, channel, uid)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5179,67 +5179,6 @@
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { modes, .. } if modes.contains('o'))), "{out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { modes, .. } if modes.contains('o'))), "{out:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A user who joins clean then switches to a nick matching the akick list is
|
|
||||||
// banned and kicked, just as if they'd joined under that nick.
|
|
||||||
#[test]
|
|
||||||
fn akick_enforced_on_nick_change() {
|
|
||||||
use echo_chanserv::ChanServ;
|
|
||||||
let path = std::env::temp_dir().join("echo-cs-akick-nick.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);
|
|
||||||
let to_cs = |e: &mut Engine, from: &str, text: &str| {
|
|
||||||
e.handle(NetEvent::Privmsg { from: from.into(), to: "42SAAAAAB".into(), text: text.into() })
|
|
||||||
};
|
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into(), host: "h1".into(), ip: "0.0.0.0".into() });
|
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
|
||||||
to_cs(&mut e, "000AAAAAB", "AKICK #c ADD evil!*@* begone");
|
|
||||||
|
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into(), host: "clean.host".into(), ip: "0.0.0.0".into() });
|
|
||||||
let out = e.handle(NetEvent::Join { uid: "000AAAAAC".into(), channel: "#c".into(), op: false });
|
|
||||||
assert!(!out.iter().any(|a| matches!(a, NetAction::Kick { .. })), "clean nick not kicked: {out:?}");
|
|
||||||
|
|
||||||
let out = e.handle(NetEvent::NickChange { uid: "000AAAAAC".into(), nick: "evil".into() });
|
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#c" && modes == "+b evil!*@*")), "{out:?}");
|
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::Kick { channel, uid, reason, .. } if channel == "#c" && uid == "000AAAAAC" && reason.starts_with("begone"))), "{out:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Being added to / removed from a channel's access list notifies the affected
|
|
||||||
// user: a memo while they're offline, a direct notice while they're online.
|
|
||||||
#[test]
|
|
||||||
fn access_change_notifies_the_affected_user() {
|
|
||||||
use echo_chanserv::ChanServ;
|
|
||||||
let path = std::env::temp_dir().join("echo-cs-access-notify.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("bob", "hunter2", 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);
|
|
||||||
let to_cs = |e: &mut Engine, from: &str, text: &str| {
|
|
||||||
e.handle(NetEvent::Privmsg { from: from.into(), to: "42SAAAAAB".into(), text: text.into() })
|
|
||||||
};
|
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "alice".into(), host: "h1".into(), ip: "0.0.0.0".into() });
|
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAA".into(), text: "IDENTIFY sesame".into() });
|
|
||||||
|
|
||||||
to_cs(&mut e, "000AAAAAB", "ACCESS #c ADD bob op");
|
|
||||||
assert_eq!(e.db.unread_memos("bob"), 1, "offline user gets a memo");
|
|
||||||
|
|
||||||
e.handle(NetEvent::UserConnect { uid: "000AAAAAC".into(), nick: "bob".into(), host: "h2".into(), ip: "0.0.0.0".into() });
|
|
||||||
e.handle(NetEvent::Privmsg { from: "000AAAAAC".into(), to: "42SAAAAAA".into(), text: "IDENTIFY hunter2".into() });
|
|
||||||
let out = to_cs(&mut e, "000AAAAAB", "ACCESS #c DEL bob");
|
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::Notice { to, text, .. } if to == "000AAAAAC" && text.contains("removed your access"))), "online user noticed: {out:?}");
|
|
||||||
assert_eq!(e.db.unread_memos("bob"), 1, "no extra memo when online");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChanServ SET: description and founder transfer, founder-gated.
|
// ChanServ SET: description and founder transfer, founder-gated.
|
||||||
#[test]
|
#[test]
|
||||||
fn chanserv_set() {
|
fn chanserv_set() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue