Migrate interpolated service messages to the t! macro (8 modules)

This commit is contained in:
Jean Chevronnet 2026-07-19 23:22:56 +00:00
parent b462d37bd5
commit d207c3d60d
No known key found for this signature in database
133 changed files with 782 additions and 702 deletions

View file

@ -566,11 +566,12 @@ impl ServiceCtx {
} }
pub fn notice(&mut self, from: &str, to: &str, text: impl Into<String>) { pub fn notice(&mut self, from: &str, to: &str, text: impl Into<String>) {
self.actions.push(NetAction::Notice { // Auto-localize: a static English reply is looked up in the catalog for this
from: from.to_string(), // command's language and swapped for its translation (English/no-catalog =
to: to.to_string(), // unchanged). Interpolated messages must use the `t!` macro so their template
text: text.into(), // — not the already-filled-in text — is the lookup key.
}); let text = render(self.lang(), &text.into(), &[]);
self.actions.push(NetAction::Notice { from: from.to_string(), to: to.to_string(), text });
} }
// IRCv3 standard replies from a service (`from` uid) to a client (`to` uid). // IRCv3 standard replies from a service (`from` uid) to a client (`to` uid).
@ -591,13 +592,14 @@ impl ServiceCtx {
} }
fn standard_reply(&mut self, kind: ReplyKind, from: &str, to: &str, command: &str, code: &str, text: impl Into<String>) { fn standard_reply(&mut self, kind: ReplyKind, from: &str, to: &str, command: &str, code: &str, text: impl Into<String>) {
let text = render(self.lang(), &text.into(), &[]); // auto-localize static replies (see `notice`)
self.actions.push(NetAction::StandardReply { self.actions.push(NetAction::StandardReply {
kind, kind,
from: from.to_string(), from: from.to_string(),
to: to.to_string(), to: to.to_string(),
command: command.to_string(), command: command.to_string(),
code: code.to_string(), code: code.to_string(),
text: text.into(), text,
}); });
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Priv, Sender, ServiceCtx, Store}; use echo_api::{t, Priv, Sender, ServiceCtx, Store};
// ASSIGN <#channel> <bot> / UNASSIGN <#channel>: put a bot in a channel (or take // ASSIGN <#channel> <bot> / UNASSIGN <#channel>: put a bot in a channel (or take
// it out). Channel founder only (or a services admin). // it out). Channel founder only (or a services admin).
@ -14,13 +14,13 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
// NOBOT reserves (un)assignment for services operators. // NOBOT reserves (un)assignment for services operators.
let is_admin = from.privs.has(Priv::Admin); let is_admin = from.privs.has(Priv::Admin);
if !is_admin && db.channel(chan).is_some_and(|c| c.nobot) { if !is_admin && db.channel(chan).is_some_and(|c| c.nobot) {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 is set \x02NOBOT\x02 — only a services operator can change its bot.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 is set \x02NOBOT\x02 — only a services operator can change its bot.", chan = chan));
return; return;
} }
if !assigning { if !assigning {
match db.unassign_bot(chan) { match db.unassign_bot(chan) {
Ok(true) => ctx.notice(me, from.uid, format!("The bot has left \x02{chan}\x02.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "The bot has left \x02{chan}\x02.", chan = chan)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no bot assigned.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has no bot assigned.", 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."),
} }
return; return;
@ -30,16 +30,16 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
let Some(target) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(bot)) else { let Some(target) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(bot)) else {
ctx.notice(me, from.uid, format!("There's no bot named \x02{bot}\x02. See \x02BOT LIST\x02.")); ctx.notice(me, from.uid, t!(ctx, "There's no bot named \x02{nick}\x02. See \x02BOT LIST\x02.", nick = bot));
return; return;
}; };
if target.private && !is_admin { if target.private && !is_admin {
ctx.notice(me, from.uid, format!("Bot \x02{}\x02 is private — only a services operator can assign it.", target.nick)); ctx.notice(me, from.uid, t!(ctx, "Bot \x02{nick}\x02 is private — only a services operator can assign it.", nick = target.nick));
return; return;
} }
let botnick = target.nick; let botnick = target.nick;
match db.assign_bot(chan, &botnick) { match db.assign_bot(chan, &botnick) {
Ok(()) => ctx.notice(me, from.uid, format!("Bot \x02{botnick}\x02 is now assigned to \x02{chan}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Bot \x02{botnick}\x02 is now assigned to \x02{chan}\x02.", botnick = botnick, 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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Priv, Sender, ServiceCtx, Store}; use echo_api::{t, Priv, Sender, ServiceCtx, Store};
// AUTOASSIGN <bot> | OFF: set the bot automatically assigned to every newly // AUTOASSIGN <bot> | OFF: set the bot automatically assigned to every newly
// registered channel, or turn auto-assignment off. Oper-only (Priv::Admin) — // registered channel, or turn auto-assignment off. Oper-only (Priv::Admin) —
@ -10,7 +10,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
let Some(&arg) = args.get(1) else { let Some(&arg) = args.get(1) else {
match db.default_bot() { match db.default_bot() {
Some(bot) => ctx.notice(me, from.uid, format!("New channels are auto-assigned \x02{bot}\x02. Use \x02AUTOASSIGN OFF\x02 to stop.")), Some(bot) => ctx.notice(me, from.uid, t!(ctx, "New channels are auto-assigned \x02{bot}\x02. Use \x02AUTOASSIGN OFF\x02 to stop.", bot = bot)),
None => ctx.notice(me, from.uid, "No bot is auto-assigned to new channels. Set one with \x02AUTOASSIGN <bot>\x02."), None => ctx.notice(me, from.uid, "No bot is auto-assigned to new channels. Set one with \x02AUTOASSIGN <bot>\x02."),
} }
return; return;
@ -24,11 +24,11 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
// Store the bot's canonical nick, so the setting survives a re-cased lookup. // Store the bot's canonical nick, so the setting survives a re-cased lookup.
let Some(bot) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(arg)) else { let Some(bot) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(arg)) else {
ctx.notice(me, from.uid, format!("There's no bot named \x02{arg}\x02. See \x02BOTLIST\x02.")); ctx.notice(me, from.uid, t!(ctx, "There's no bot named \x02{nick}\x02. See \x02BOTLIST\x02.", nick = arg));
return; return;
}; };
match db.set_default_bot(Some(&bot.nick)) { match db.set_default_bot(Some(&bot.nick)) {
Ok(()) => ctx.notice(me, from.uid, format!("New channels will be auto-assigned \x02{}\x02.", bot.nick)), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "New channels will be auto-assigned \x02{nick}\x02.", nick = bot.nick)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{ChanError, Sender, ServiceCtx, Store}; use echo_api::{t, ChanError, Sender, ServiceCtx, Store};
// BADWORDS <#channel> ADD <regex> | DEL <regex> | LIST | CLEAR: manage the // BADWORDS <#channel> ADD <regex> | DEL <regex> | LIST | CLEAR: manage the
// channel's badword patterns. Each entry is a regular expression, so a channel // channel's badword patterns. Each entry is a regular expression, so a channel
@ -21,9 +21,9 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
let pattern = args[3..].join(" "); let pattern = args[3..].join(" ");
match db.badword_add(chan, &pattern) { match db.badword_add(chan, &pattern) {
Ok(true) => ctx.notice(me, from.uid, format!("Added badword pattern to \x02{chan}\x02: {pattern}")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Added badword pattern to \x02{chan}\x02: {pattern}", chan = chan, pattern = pattern)),
Ok(false) => ctx.notice(me, from.uid, "That pattern is already on the list."), Ok(false) => ctx.notice(me, from.uid, "That pattern is already on the list."),
Err(ChanError::InvalidPattern) => ctx.notice(me, from.uid, format!("\x02{pattern}\x02 isn't a valid regular expression.")), Err(ChanError::InvalidPattern) => ctx.notice(me, from.uid, t!(ctx, "\x02{pattern}\x02 isn't a valid regular expression.", pattern = pattern)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
} }
} }
@ -34,30 +34,30 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
let pattern = args[3..].join(" "); let pattern = args[3..].join(" ");
match db.badword_del(chan, &pattern) { match db.badword_del(chan, &pattern) {
Ok(true) => ctx.notice(me, from.uid, format!("Removed badword pattern from \x02{chan}\x02.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Removed badword pattern from \x02{chan}\x02.", chan = chan)),
Ok(false) => ctx.notice(me, from.uid, "That pattern isn't on the list."), Ok(false) => ctx.notice(me, from.uid, "That pattern isn't on the list."),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
} }
} }
Some("CLEAR") => match db.badword_clear(chan) { Some("CLEAR") => match db.badword_clear(chan) {
Ok(n) => ctx.notice(me, from.uid, format!("Cleared \x02{n}\x02 badword pattern(s) from \x02{chan}\x02.")), Ok(n) => ctx.notice(me, from.uid, t!(ctx, "Cleared \x02{n}\x02 badword pattern(s) from \x02{chan}\x02.", n = n, chan = chan)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
}, },
None | Some("LIST") => { None | Some("LIST") => {
let words = db.badwords(chan); let words = db.badwords(chan);
if words.is_empty() { if words.is_empty() {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no badword patterns.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has no badword patterns.", chan = chan));
return; return;
} }
ctx.notice(me, from.uid, format!("Badword patterns for \x02{chan}\x02 ({}):", words.len())); ctx.notice(me, from.uid, t!(ctx, "Badword patterns for \x02{chan}\x02 ({count}):", chan = chan, count = words.len()));
for (i, w) in words.iter().enumerate() { for (i, w) in words.iter().enumerate() {
ctx.notice(me, from.uid, format!(" {}. {w}", i + 1)); ctx.notice(me, from.uid, t!(ctx, " {num}. {word}", num = i + 1, word = w));
} }
} }
Some(other) => ctx.notice(me, from.uid, format!("Unknown BADWORDS command \x02{other}\x02. Use ADD, DEL, LIST or CLEAR.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "Unknown BADWORDS command \x02{other}\x02. Use ADD, DEL, LIST or CLEAR.", other = other)),
} }
} }
fn reg_error(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx) { fn reg_error(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx) {
ctx.notice(me, from.uid, format!("Couldn't update \x02{chan}\x02 — please try again in a moment.")); ctx.notice(me, from.uid, t!(ctx, "Couldn't update \x02{chan}\x02 — please try again in a moment.", chan = chan));
} }

View file

@ -1,4 +1,4 @@
use echo_api::{ChanError, Priv, Sender, ServiceCtx, Store}; use echo_api::{t, ChanError, Priv, Sender, ServiceCtx, Store};
// BOT ADD <nick> <user> <host> [gecos] | BOT DEL <nick> | BOT LIST — manage the // BOT ADD <nick> <user> <host> [gecos] | BOT DEL <nick> | BOT LIST — manage the
// bot registry. Administering bots is oper-only (Priv::Admin). // bot registry. Administering bots is oper-only (Priv::Admin).
@ -15,8 +15,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
let gecos = if args.len() > 5 { args[5..].join(" ") } else { "Service Bot".to_string() }; let gecos = if args.len() > 5 { args[5..].join(" ") } else { "Service Bot".to_string() };
match db.bot_add(nick, user, host, &gecos) { match db.bot_add(nick, user, host, &gecos) {
Ok(()) => ctx.notice(me, from.uid, format!("Bot \x02{nick}\x02 (\x02{user}@{host}\x02) added.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Bot \x02{nick}\x02 (\x02{user}@{host}\x02) added.", nick = nick, user = user, host = host)),
Err(_) => ctx.notice(me, from.uid, format!("A bot named \x02{nick}\x02 already exists, or that didn't work.")), Err(_) => ctx.notice(me, from.uid, t!(ctx, "A bot named \x02{nick}\x02 already exists, or that didn't work.", nick = nick)),
} }
} }
Some("CHANGE") => { Some("CHANGE") => {
@ -26,15 +26,15 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
// Omitted fields keep the bot's current values. // Omitted fields keep the bot's current values.
let Some(cur) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(old)) else { let Some(cur) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(old)) else {
ctx.notice(me, from.uid, format!("There's no bot named \x02{old}\x02.")); ctx.notice(me, from.uid, t!(ctx, "There's no bot named \x02{nick}\x02.", nick = old));
return; return;
}; };
let user = args.get(4).map(|s| s.to_string()).unwrap_or(cur.user); let user = args.get(4).map(|s| s.to_string()).unwrap_or(cur.user);
let host = args.get(5).map(|s| s.to_string()).unwrap_or(cur.host); let host = args.get(5).map(|s| s.to_string()).unwrap_or(cur.host);
let gecos = if args.len() > 6 { args[6..].join(" ") } else { cur.gecos }; let gecos = if args.len() > 6 { args[6..].join(" ") } else { cur.gecos };
match db.bot_change(old, newnick, &user, &host, &gecos) { match db.bot_change(old, newnick, &user, &host, &gecos) {
Ok(()) => ctx.notice(me, from.uid, format!("Bot \x02{old}\x02 is now \x02{newnick}\x02 (\x02{user}@{host}\x02).")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Bot \x02{old}\x02 is now \x02{newnick}\x02 (\x02{user}@{host}\x02).", old = old, newnick = newnick, user = user, host = host)),
Err(ChanError::Exists) => ctx.notice(me, from.uid, format!("A bot named \x02{newnick}\x02 already exists.")), Err(ChanError::Exists) => ctx.notice(me, from.uid, t!(ctx, "A bot named \x02{newnick}\x02 already exists.", newnick = newnick)),
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."),
} }
} }
@ -45,14 +45,14 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
if nick == "*" { if nick == "*" {
match db.bot_del_all() { match db.bot_del_all() {
Ok(n) => ctx.notice(me, from.uid, format!("Removed all \x02{n}\x02 bot(s).")), Ok(n) => ctx.notice(me, from.uid, t!(ctx, "Removed all \x02{n}\x02 bot(s).", n = n)),
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."),
} }
return; return;
} }
match db.bot_del(nick) { match db.bot_del(nick) {
Ok(true) => ctx.notice(me, from.uid, format!("Bot \x02{nick}\x02 deleted.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Bot \x02{nick}\x02 deleted.", nick = nick)),
Ok(false) => ctx.notice(me, from.uid, format!("There's no bot named \x02{nick}\x02.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "There's no bot named \x02{nick}\x02.", nick = nick)),
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."),
} }
} }
@ -62,12 +62,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
ctx.notice(me, from.uid, "No bots have been added yet. Add one with \x02BOT ADD\x02."); ctx.notice(me, from.uid, "No bots have been added yet. Add one with \x02BOT ADD\x02.");
return; return;
} }
ctx.notice(me, from.uid, format!("Bots ({}):", bots.len())); ctx.notice(me, from.uid, t!(ctx, "Bots ({count}):", count = bots.len()));
for b in &bots { for b in &bots {
let flag = if b.private { " \x02[private]\x02" } else { "" }; let flag = if b.private { " \x02[private]\x02" } else { "" };
ctx.notice(me, from.uid, format!(" \x02{}\x02 ({}@{}) — {}{flag}", b.nick, b.user, b.host, b.gecos)); ctx.notice(me, from.uid, t!(ctx, " \x02{nick}\x02 ({user}@{host}) — {gecos}{flag}", nick = b.nick, user = b.user, host = b.host, gecos = b.gecos, flag = flag));
} }
} }
Some(other) => ctx.notice(me, from.uid, format!("Unknown BOT command \x02{other}\x02. Use \x02ADD\x02, \x02CHANGE\x02, \x02DEL\x02 or \x02LIST\x02.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "Unknown BOT command \x02{other}\x02. Use \x02ADD\x02, \x02CHANGE\x02, \x02DEL\x02 or \x02LIST\x02.", other = other)),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Priv, Sender, ServiceCtx, Store}; use echo_api::{t, Priv, Sender, ServiceCtx, Store};
// BOTLIST: show the bots a channel founder can assign. Available to everyone, // BOTLIST: show the bots a channel founder can assign. Available to everyone,
// unlike \x02BOT LIST\x02 (operator bot administration). Private bots are hidden // unlike \x02BOT LIST\x02 (operator bot administration). Private bots are hidden
@ -10,10 +10,10 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
ctx.notice(me, from.uid, "No bots are available."); ctx.notice(me, from.uid, "No bots are available.");
return; return;
} }
ctx.notice(me, from.uid, format!("Available bots ({}):", bots.len())); ctx.notice(me, from.uid, t!(ctx, "Available bots ({count}):", count = bots.len()));
for b in &bots { for b in &bots {
let flag = if b.private { " \x02[private]\x02" } else { "" }; let flag = if b.private { " \x02[private]\x02" } else { "" };
ctx.notice(me, from.uid, format!(" \x02{}\x02 ({}@{}){flag}", b.nick, b.user, b.host)); ctx.notice(me, from.uid, t!(ctx, " \x02{nick}\x02 ({user}@{host}){flag}", nick = b.nick, user = b.user, host = b.host, flag = flag));
} }
ctx.notice(me, from.uid, "Assign one with \x02ASSIGN <#channel> <bot>\x02."); ctx.notice(me, from.uid, "Assign one with \x02ASSIGN <#channel> <bot>\x02.");
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// COPY <#source> <#dest>: copy a channel's bot configuration — kickers, // COPY <#source> <#dest>: copy a channel's bot configuration — kickers,
// badwords, greet and nobot — onto another. Requires founder-or-admin on both. // badwords, greet and nobot — onto another. Requires founder-or-admin on both.
@ -11,7 +11,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
match db.copy_bot_config(src, dst) { match db.copy_bot_config(src, dst) {
Ok(()) => ctx.notice(me, from.uid, format!("Copied \x02{src}\x02's bot settings (kickers, badwords, greet, nobot) to \x02{dst}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Copied \x02{src}\x02's bot settings (kickers, badwords, greet, nobot) to \x02{dst}\x02.", src = src, dst = dst)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// INFO <bot> — describe a bot and list the channels it serves. // INFO <bot> — describe a bot and list the channels it serves.
// INFO <#channel> — show which bot (if any) is assigned to a channel. // INFO <#channel> — show which bot (if any) is assigned to a channel.
@ -10,12 +10,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if target.starts_with('#') { if target.starts_with('#') {
let Some(chan) = db.channel(target) else { let Some(chan) = db.channel(target) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = target));
return; return;
}; };
match &chan.assigned_bot { match &chan.assigned_bot {
Some(bot) => ctx.notice(me, from.uid, format!("\x02{target}\x02 is served by bot \x02{bot}\x02.")), Some(bot) => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 is served by bot \x02{bot}\x02.", chan = target, bot = bot)),
None => ctx.notice(me, from.uid, format!("\x02{target}\x02 has no bot assigned. Assign one with \x02ASSIGN\x02 {target} <bot>.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has no bot assigned. Assign one with \x02ASSIGN\x02 {chan} <bot>.", chan = target)),
} }
// The BotServ options in effect on this channel. // The BotServ options in effect on this channel.
let mut opts = Vec::new(); let mut opts = Vec::new();
@ -29,12 +29,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
opts.push("nobot"); opts.push("nobot");
} }
let summary = if opts.is_empty() { "none".to_string() } else { opts.join(", ") }; let summary = if opts.is_empty() { "none".to_string() } else { opts.join(", ") };
ctx.notice(me, from.uid, format!(" Options: {summary}")); ctx.notice(me, from.uid, t!(ctx, " Options: {summary}", summary = summary));
return; return;
} }
let Some(bot) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(target)) else { let Some(bot) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(target)) else {
ctx.notice(me, from.uid, format!("There's no bot named \x02{target}\x02. See \x02BOT LIST\x02.")); ctx.notice(me, from.uid, t!(ctx, "There's no bot named \x02{nick}\x02. See \x02BOT LIST\x02.", nick = target));
return; return;
}; };
let channels: Vec<String> = db let channels: Vec<String> = db
@ -45,11 +45,11 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
.collect(); .collect();
let privacy = if bot.private { " (private)" } else { "" }; let privacy = if bot.private { " (private)" } else { "" };
ctx.notice(me, from.uid, format!("Bot \x02{}\x02{}@{}{privacy}", bot.nick, bot.user, bot.host)); ctx.notice(me, from.uid, t!(ctx, "Bot \x02{nick}\x02 — {user}@{host}{privacy}", nick = bot.nick, user = bot.user, host = bot.host, privacy = privacy));
ctx.notice(me, from.uid, format!(" Real name: {}", bot.gecos)); ctx.notice(me, from.uid, t!(ctx, " Real name: {gecos}", gecos = bot.gecos));
if channels.is_empty() { if channels.is_empty() {
ctx.notice(me, from.uid, " Not assigned to any channel."); ctx.notice(me, from.uid, " Not assigned to any channel.");
} else { } else {
ctx.notice(me, from.uid, format!(" Serving {} channel(s): {}", channels.len(), channels.join(", "))); ctx.notice(me, from.uid, t!(ctx, " Serving {count} channel(s): {list}", count = channels.len(), list = channels.join(", ")));
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Kicker, Sender, ServiceCtx, Store}; use echo_api::{t, Kicker, Sender, ServiceCtx, Store};
// KICK <#channel> <type> {ON|OFF} [params]: configure the bot's kickers. // KICK <#channel> <type> {ON|OFF} [params]: configure the bot's kickers.
// Types: CAPS [min [percent]], BOLDS, COLORS, UNDERLINES, REVERSES, ITALICS, // Types: CAPS [min [percent]], BOLDS, COLORS, UNDERLINES, REVERSES, ITALICS,
@ -16,8 +16,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if kind.eq_ignore_ascii_case("TTB") { if kind.eq_ignore_ascii_case("TTB") {
match args.get(3).and_then(|s| s.parse::<u16>().ok()) { match args.get(3).and_then(|s| s.parse::<u16>().ok()) {
Some(n) => match db.set_ttb(chan, n) { Some(n) => match db.set_ttb(chan, n) {
Ok(()) if n == 0 => ctx.notice(me, from.uid, format!("The bot will only kick (not ban) in \x02{chan}\x02.")), Ok(()) if n == 0 => ctx.notice(me, from.uid, t!(ctx, "The bot will only kick (not ban) in \x02{chan}\x02.", chan = chan)),
Ok(()) => ctx.notice(me, from.uid, format!("The bot will ban a user after \x02{n}\x02 kick(s) in \x02{chan}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "The bot will ban a user after \x02{n}\x02 kick(s) in \x02{chan}\x02.", n = n, chan = chan)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
}, },
None => ctx.notice(me, from.uid, "Syntax: KICK <#channel> TTB <number>"), None => ctx.notice(me, from.uid, "Syntax: KICK <#channel> TTB <number>"),
@ -34,7 +34,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
let text = args[3..].join(" "); let text = args[3..].join(" ");
match db.kicker_test(chan, &text) { match db.kicker_test(chan, &text) {
Some(reason) => ctx.notice(me, from.uid, format!("That line \x02would be kicked\x02: {reason}")), Some(reason) => ctx.notice(me, from.uid, t!(ctx, "That line \x02would be kicked\x02: {reason}", reason = reason)),
None => ctx.notice(me, from.uid, "That line trips no content kicker (flood/repeat depend on timing and repetition, so they aren't tested here)."), None => ctx.notice(me, from.uid, "That line trips no content kicker (flood/repeat depend on timing and repetition, so they aren't tested here)."),
} }
return; return;
@ -55,7 +55,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
let min = args.get(4).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0); let min = args.get(4).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
let percent = args.get(5).and_then(|s| s.parse::<u16>().ok()).filter(|p| (1..=100).contains(p)).unwrap_or(0); let percent = args.get(5).and_then(|s| s.parse::<u16>().ok()).filter(|p| (1..=100).contains(p)).unwrap_or(0);
match db.set_caps_kicker(chan, min, percent) { match db.set_caps_kicker(chan, min, percent) {
Ok(()) => ctx.notice(me, from.uid, format!("The bot will now kick for \x02caps\x02 in \x02{chan}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "The bot will now kick for \x02caps\x02 in \x02{chan}\x02.", chan = chan)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
} }
} else { } else {
@ -68,7 +68,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
let lines = args.get(4).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0); let lines = args.get(4).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
let secs = args.get(5).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0); let secs = args.get(5).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
match db.set_flood_kicker(chan, lines, secs) { match db.set_flood_kicker(chan, lines, secs) {
Ok(()) => ctx.notice(me, from.uid, format!("The bot will now kick for \x02flooding\x02 in \x02{chan}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "The bot will now kick for \x02flooding\x02 in \x02{chan}\x02.", chan = chan)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
} }
} else { } else {
@ -80,7 +80,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if on { if on {
let times = args.get(4).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0); let times = args.get(4).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
match db.set_repeat_kicker(chan, times) { match db.set_repeat_kicker(chan, times) {
Ok(()) => ctx.notice(me, from.uid, format!("The bot will now kick for \x02repeating\x02 in \x02{chan}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "The bot will now kick for \x02repeating\x02 in \x02{chan}\x02.", chan = chan)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
} }
} else { } else {
@ -100,7 +100,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
"DONTKICKOPS" => Kicker::DontKickOps, "DONTKICKOPS" => Kicker::DontKickOps,
"DONTKICKVOICES" => Kicker::DontKickVoices, "DONTKICKVOICES" => Kicker::DontKickVoices,
other => { other => {
ctx.notice(me, from.uid, format!("Unknown kicker \x02{other}\x02. Try CAPS, FLOOD, REPEAT, BADWORDS, BOLDS, COLORS, UNDERLINES, REVERSES, ITALICS, WARN, DONTKICKOPS or DONTKICKVOICES.")); ctx.notice(me, from.uid, t!(ctx, "Unknown kicker \x02{other}\x02. Try CAPS, FLOOD, REPEAT, BADWORDS, BOLDS, COLORS, UNDERLINES, REVERSES, ITALICS, WARN, DONTKICKOPS or DONTKICKVOICES.", other = other));
return; return;
} }
}; };
@ -126,18 +126,18 @@ fn label(kicker: Kicker) -> &'static str {
fn toggle(me: &str, from: &Sender, chan: &str, kicker: Kicker, on: bool, ctx: &mut ServiceCtx, db: &mut dyn Store) { fn toggle(me: &str, from: &Sender, chan: &str, kicker: Kicker, on: bool, ctx: &mut ServiceCtx, db: &mut dyn Store) {
match db.set_kicker(chan, kicker, on) { match db.set_kicker(chan, kicker, on) {
Ok(()) if kicker == Kicker::DontKickOps && on => ctx.notice(me, from.uid, format!("The bot will no longer kick channel operators in \x02{chan}\x02.")), Ok(()) if kicker == Kicker::DontKickOps && on => ctx.notice(me, from.uid, t!(ctx, "The bot will no longer kick channel operators in \x02{chan}\x02.", chan = chan)),
Ok(()) if kicker == Kicker::DontKickOps => ctx.notice(me, from.uid, format!("The bot may again kick channel operators in \x02{chan}\x02.")), Ok(()) if kicker == Kicker::DontKickOps => ctx.notice(me, from.uid, t!(ctx, "The bot may again kick channel operators in \x02{chan}\x02.", chan = chan)),
Ok(()) if kicker == Kicker::DontKickVoices && on => ctx.notice(me, from.uid, format!("The bot will no longer kick voiced users in \x02{chan}\x02.")), Ok(()) if kicker == Kicker::DontKickVoices && on => ctx.notice(me, from.uid, t!(ctx, "The bot will no longer kick voiced users in \x02{chan}\x02.", chan = chan)),
Ok(()) if kicker == Kicker::DontKickVoices => ctx.notice(me, from.uid, format!("The bot may again kick voiced users in \x02{chan}\x02.")), Ok(()) if kicker == Kicker::DontKickVoices => ctx.notice(me, from.uid, t!(ctx, "The bot may again kick voiced users in \x02{chan}\x02.", chan = chan)),
Ok(()) if kicker == Kicker::Warn && on => ctx.notice(me, from.uid, format!("The bot will warn once before the first kick in \x02{chan}\x02.")), Ok(()) if kicker == Kicker::Warn && on => ctx.notice(me, from.uid, t!(ctx, "The bot will warn once before the first kick in \x02{chan}\x02.", chan = chan)),
Ok(()) if kicker == Kicker::Warn => ctx.notice(me, from.uid, format!("The bot will kick without warning in \x02{chan}\x02.")), Ok(()) if kicker == Kicker::Warn => ctx.notice(me, from.uid, t!(ctx, "The bot will kick without warning in \x02{chan}\x02.", chan = chan)),
Ok(()) if on => ctx.notice(me, from.uid, format!("The \x02{}\x02 kicker is now on in \x02{chan}\x02.", label(kicker))), Ok(()) if on => ctx.notice(me, from.uid, t!(ctx, "The \x02{kicker}\x02 kicker is now on in \x02{chan}\x02.", kicker = label(kicker), chan = chan)),
Ok(()) => ctx.notice(me, from.uid, format!("The \x02{}\x02 kicker is now off in \x02{chan}\x02.", label(kicker))), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "The \x02{kicker}\x02 kicker is now off in \x02{chan}\x02.", kicker = label(kicker), chan = chan)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
} }
} }
fn reg_error(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx) { fn reg_error(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx) {
ctx.notice(me, from.uid, format!("Couldn't update \x02{chan}\x02 — please try again in a moment.")); ctx.notice(me, from.uid, t!(ctx, "Couldn't update \x02{chan}\x02 — please try again in a moment.", chan = chan));
} }

View file

@ -2,7 +2,7 @@
//! and (in later slices) run fantasy commands. `lib.rs` holds the dispatcher; //! and (in later slices) run fantasy commands. `lib.rs` holds the dispatcher;
//! each command lives in its own file, matching NickServ/ChanServ. //! each command lives in its own file, matching NickServ/ChanServ.
use echo_api::{HelpEntry, NetView, Priv, Sender, Service, ServiceCtx, Store}; use echo_api::{t, HelpEntry, NetView, Priv, Sender, Service, ServiceCtx, Store};
#[path = "bot.rs"] #[path = "bot.rs"]
mod bot; mod bot;
@ -31,11 +31,11 @@ mod trigger;
// founder or a services admin. Notices and returns false on failure. // founder or a services admin. Notices and returns false on failure.
fn require_channel_admin(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool { fn require_channel_admin(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool {
let Some(founder) = db.channel(chan).map(|c| c.founder) else { let Some(founder) = db.channel(chan).map(|c| c.founder) else {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return false; return false;
}; };
if from.account != Some(founder.as_str()) && !from.privs.has(Priv::Admin) { if from.account != Some(founder.as_str()) && !from.privs.has(Priv::Admin) {
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can change that.")); ctx.notice(me, from.uid, t!(ctx, "Only \x02{chan}\x02's founder can change that.", chan = chan));
return false; return false;
} }
true true
@ -95,7 +95,7 @@ impl Service for BotServ {
Some("COPY") => copy::handle(me, from, args, ctx, db), Some("COPY") => copy::handle(me, from, args, ctx, db),
Some("TRIGGER") => trigger::handle(me, from, args, ctx, db), Some("TRIGGER") => trigger::handle(me, from, args, ctx, db),
Some("HELP") | None => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()), Some("HELP") | None => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()),
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "I don't know the command \x02{other}\x02. Try \x02HELP\x02.", other = other)),
} }
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{NetView, Priv, Sender, ServiceCtx, Store}; use echo_api::{t, NetView, Priv, Sender, ServiceCtx, Store};
// SAY <#channel> <text> — make the channel's assigned bot say something. // SAY <#channel> <text> — make the channel's assigned bot say something.
// ACT <#channel> <text> — the same, as a CTCP ACTION (/me). // ACT <#channel> <text> — the same, as a CTCP ACTION (/me).
@ -6,27 +6,27 @@ use echo_api::{NetView, Priv, Sender, ServiceCtx, Store};
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store, action: bool) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store, action: bool) {
let verb = if action { "ACT" } else { "SAY" }; let verb = if action { "ACT" } else { "SAY" };
if args.len() < 3 { if args.len() < 3 {
ctx.notice(me, from.uid, format!("Syntax: {verb} <#channel> <text>")); ctx.notice(me, from.uid, t!(ctx, "Syntax: {verb} <#channel> <text>", verb = verb));
return; return;
} }
let chan = args[1]; let chan = args[1];
let text = args[2..].join(" "); let text = args[2..].join(" ");
let Some(info) = db.channel(chan) else { let Some(info) = db.channel(chan) else {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
}; };
let allowed = from.privs.has(Priv::Admin) || from.account.is_some_and(|a| info.is_op(a)); let allowed = from.privs.has(Priv::Admin) || from.account.is_some_and(|a| info.is_op(a));
if !allowed { if !allowed {
ctx.notice(me, from.uid, format!("Access denied — you need operator access in \x02{chan}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Access denied — you need operator access in \x02{chan}\x02.", chan = chan));
return; return;
} }
let Some(bot) = info.assigned_bot else { let Some(bot) = info.assigned_bot else {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no bot assigned. Assign one with \x02ASSIGN\x02 {chan} <bot>.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has no bot assigned. Assign one with \x02ASSIGN\x02 {chan} <bot>.", chan = chan));
return; return;
}; };
let Some(botuid) = net.uid_by_nick(&bot) else { let Some(botuid) = net.uid_by_nick(&bot) else {
ctx.notice(me, from.uid, format!("Bot \x02{bot}\x02 isn't on the network right now.")); ctx.notice(me, from.uid, t!(ctx, "Bot \x02{bot}\x02 isn't on the network right now.", bot = bot));
return; return;
}; };
let botuid = botuid.to_string(); let botuid = botuid.to_string();

View file

@ -1,4 +1,4 @@
use echo_api::{parse_duration, ChanSetting, Priv, Sender, ServiceCtx, Store}; use echo_api::{parse_duration, t, ChanSetting, Priv, Sender, ServiceCtx, Store};
// SET <#channel> <option> <value>: per-channel bot options (founder-or-admin) — // SET <#channel> <option> <value>: per-channel bot options (founder-or-admin) —
// GREET <on|off>, BANEXPIRE <duration|off>, NOBOT <on|off>. Also // GREET <on|off>, BANEXPIRE <duration|off>, NOBOT <on|off>. Also
@ -24,14 +24,14 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
(false, _) => { (false, _) => {
ctx.notice(me, from.uid, format!("Unknown bot option \x02{option}\x02. Available: \x02PRIVATE\x02.")); ctx.notice(me, from.uid, t!(ctx, "Unknown bot option \x02{option}\x02. Available: \x02PRIVATE\x02.", option = option));
return; return;
} }
}; };
match db.bot_set_private(target, on) { match db.bot_set_private(target, on) {
Ok(true) if on => ctx.notice(me, from.uid, format!("Bot \x02{target}\x02 is now private (operators only).")), Ok(true) if on => ctx.notice(me, from.uid, t!(ctx, "Bot \x02{nick}\x02 is now private (operators only).", nick = target)),
Ok(true) => ctx.notice(me, from.uid, format!("Bot \x02{target}\x02 is now public.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Bot \x02{nick}\x02 is now public.", nick = target)),
Ok(false) => ctx.notice(me, from.uid, format!("There's no bot named \x02{target}\x02.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "There's no bot named \x02{nick}\x02.", nick = target)),
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."),
} }
return; return;
@ -46,8 +46,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if option.eq_ignore_ascii_case("VOTEKICK") { if option.eq_ignore_ascii_case("VOTEKICK") {
match args.get(3).and_then(|s| s.parse::<u16>().ok()) { match args.get(3).and_then(|s| s.parse::<u16>().ok()) {
Some(n) => match db.set_votekick(chan, n) { Some(n) => match db.set_votekick(chan, n) {
Ok(()) if n == 0 => ctx.notice(me, from.uid, format!("\x02!votekick\x02 is now disabled in \x02{chan}\x02.")), Ok(()) if n == 0 => ctx.notice(me, from.uid, t!(ctx, "\x02!votekick\x02 is now disabled in \x02{chan}\x02.", chan = chan)),
Ok(()) => ctx.notice(me, from.uid, format!("\x02{n}\x02 vote(s) will now carry a \x02!votekick\x02/\x02!voteban\x02 in \x02{chan}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "\x02{n}\x02 vote(s) will now carry a \x02!votekick\x02/\x02!voteban\x02 in \x02{chan}\x02.", n = n, 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."),
}, },
None => ctx.notice(me, from.uid, "Syntax: SET <#channel> VOTEKICK <number> (0 to disable)"), None => ctx.notice(me, from.uid, "Syntax: SET <#channel> VOTEKICK <number> (0 to disable)"),
@ -67,14 +67,14 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
match parse_duration(arg) { match parse_duration(arg) {
Some(s) => s.min(u32::MAX as u64) as u32, Some(s) => s.min(u32::MAX as u64) as u32,
None => { None => {
ctx.notice(me, from.uid, format!("\x02{arg}\x02 isn't a valid duration. Try 30m, 2h, 7d or \x02off\x02.")); ctx.notice(me, from.uid, t!(ctx, "\x02{arg}\x02 isn't a valid duration. Try 30m, 2h, 7d or \x02off\x02.", arg = arg));
return; return;
} }
} }
}; };
match db.set_ban_expire(chan, secs) { match db.set_ban_expire(chan, secs) {
Ok(()) if secs == 0 => ctx.notice(me, from.uid, format!("Kicker bans in \x02{chan}\x02 won't expire automatically.")), Ok(()) if secs == 0 => ctx.notice(me, from.uid, t!(ctx, "Kicker bans in \x02{chan}\x02 won't expire automatically.", chan = chan)),
Ok(()) => ctx.notice(me, from.uid, format!("Kicker bans in \x02{chan}\x02 will expire after \x02{arg}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Kicker bans in \x02{chan}\x02 will expire after \x02{arg}\x02.", chan = chan, arg = arg)),
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."),
} }
return; return;
@ -90,15 +90,15 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
match option.to_ascii_uppercase().as_str() { match option.to_ascii_uppercase().as_str() {
"GREET" => match db.set_channel_setting(chan, ChanSetting::BotGreet, on) { "GREET" => match db.set_channel_setting(chan, ChanSetting::BotGreet, on) {
Ok(()) if on => ctx.notice(me, from.uid, format!("Greet messages are now \x02on\x02 in \x02{chan}\x02.")), Ok(()) if on => ctx.notice(me, from.uid, t!(ctx, "Greet messages are now \x02on\x02 in \x02{chan}\x02.", chan = chan)),
Ok(()) => ctx.notice(me, from.uid, format!("Greet messages are now \x02off\x02 in \x02{chan}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Greet messages are now \x02off\x02 in \x02{chan}\x02.", 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."),
}, },
"NOBOT" => match db.set_channel_setting(chan, ChanSetting::NoBot, on) { "NOBOT" => match db.set_channel_setting(chan, ChanSetting::NoBot, on) {
Ok(()) if on => ctx.notice(me, from.uid, format!("Only operators may (un)assign a bot in \x02{chan}\x02 now.")), Ok(()) if on => ctx.notice(me, from.uid, t!(ctx, "Only operators may (un)assign a bot in \x02{chan}\x02 now.", chan = chan)),
Ok(()) => ctx.notice(me, from.uid, format!("The founder may (un)assign a bot in \x02{chan}\x02 again.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "The founder may (un)assign a bot in \x02{chan}\x02 again.", 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."),
}, },
other => ctx.notice(me, from.uid, format!("Unknown option \x02{other}\x02. Available: \x02GREET\x02, \x02BANEXPIRE\x02, \x02NOBOT\x02.")), other => ctx.notice(me, from.uid, t!(ctx, "Unknown option \x02{other}\x02. Available: \x02GREET\x02, \x02BANEXPIRE\x02, \x02NOBOT\x02.", other = other)),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{ChanError, Sender, ServiceCtx, Store}; use echo_api::{t, ChanError, Sender, ServiceCtx, Store};
// TRIGGER <#channel> ADD <regex>|<response> | DEL <num> | LIST | CLEAR: manage // TRIGGER <#channel> ADD <regex>|<response> | DEL <num> | LIST | CLEAR: manage
// the channel's auto-responses. When a line matches <regex>, the assigned bot // the channel's auto-responses. When a line matches <regex>, the assigned bot
@ -35,9 +35,9 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
match db.trigger_add(chan, pattern, response, cooldown) { match db.trigger_add(chan, pattern, response, cooldown) {
Ok(true) => ctx.notice(me, from.uid, format!("Added a trigger to \x02{chan}\x02.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Added a trigger to \x02{chan}\x02.", chan = chan)),
Ok(false) => ctx.notice(me, from.uid, "That pattern already has a trigger."), Ok(false) => ctx.notice(me, from.uid, "That pattern already has a trigger."),
Err(ChanError::InvalidPattern) => ctx.notice(me, from.uid, format!("\x02{pattern}\x02 isn't a valid regular expression.")), Err(ChanError::InvalidPattern) => ctx.notice(me, from.uid, t!(ctx, "\x02{pattern}\x02 isn't a valid regular expression.", pattern = pattern)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
} }
} }
@ -47,31 +47,31 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
match db.trigger_del(chan, n) { match db.trigger_del(chan, n) {
Ok(true) => ctx.notice(me, from.uid, format!("Trigger #\x02{n}\x02 removed from \x02{chan}\x02.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Trigger #\x02{n}\x02 removed from \x02{chan}\x02.", n = n, chan = chan)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no trigger #\x02{n}\x02.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has no trigger #\x02{n}\x02.", chan = chan, n = n)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
} }
} }
Some("CLEAR") => match db.trigger_clear(chan) { Some("CLEAR") => match db.trigger_clear(chan) {
Ok(n) => ctx.notice(me, from.uid, format!("Cleared \x02{n}\x02 trigger(s) from \x02{chan}\x02.")), Ok(n) => ctx.notice(me, from.uid, t!(ctx, "Cleared \x02{n}\x02 trigger(s) from \x02{chan}\x02.", n = n, chan = chan)),
Err(_) => reg_error(me, from, chan, ctx), Err(_) => reg_error(me, from, chan, ctx),
}, },
None | Some("LIST") => { None | Some("LIST") => {
let triggers = db.triggers(chan); let triggers = db.triggers(chan);
if triggers.is_empty() { if triggers.is_empty() {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no triggers.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has no triggers.", chan = chan));
return; return;
} }
ctx.notice(me, from.uid, format!("Triggers for \x02{chan}\x02 ({}):", triggers.len())); ctx.notice(me, from.uid, t!(ctx, "Triggers for \x02{chan}\x02 ({count}):", chan = chan, count = triggers.len()));
for (i, t) in triggers.iter().enumerate() { for (i, tr) in triggers.iter().enumerate() {
let cd = if t.cooldown > 0 { format!(" (cooldown {}s)", t.cooldown) } else { String::new() }; let cd = if tr.cooldown > 0 { t!(ctx, " (cooldown {secs}s)", secs = tr.cooldown) } else { String::new() };
ctx.notice(me, from.uid, format!(" {}. {} \x02\x02 {}{cd}", i + 1, t.pattern, t.response)); ctx.notice(me, from.uid, t!(ctx, " {num}. {pattern} \x02\x02 {response}{cd}", num = i + 1, pattern = tr.pattern, response = tr.response, cd = cd));
} }
} }
Some(other) => ctx.notice(me, from.uid, format!("Unknown TRIGGER command \x02{other}\x02. Use ADD, DEL, LIST or CLEAR.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "Unknown TRIGGER command \x02{other}\x02. Use ADD, DEL, LIST or CLEAR.", other = other)),
} }
} }
fn reg_error(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx) { fn reg_error(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx) {
ctx.notice(me, from.uid, format!("Couldn't update \x02{chan}\x02 — please try again in a moment.")); ctx.notice(me, from.uid, t!(ctx, "Couldn't update \x02{chan}\x02 — please try again in a moment.", chan = chan));
} }

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
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
// covers anything finer. Kept in step with the XOP presets in `level_caps`. // covers anything finer. Kept in step with the XOP presets in `level_caps`.
@ -13,12 +14,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() { match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() {
None | Some("LIST") => match db.channel(chan) { None | Some("LIST") => match db.channel(chan) {
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan)),
Some(info) => { Some(info) => {
ctx.notice(me, from.uid, format!("Access list for \x02{}\x02:", info.name)); ctx.notice(me, from.uid, t!(ctx, "Access list for \x02{name}\x02:", name = info.name));
ctx.notice(me, from.uid, format!(" \x02{}\x02 (founder)", info.founder)); ctx.notice(me, from.uid, t!(ctx, " \x02{founder}\x02 (founder)", founder = info.founder));
for a in &info.access { for a in &info.access {
ctx.notice(me, from.uid, format!(" \x02{}\x02 ({})", a.account, a.level)); ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02 ({level})", account = a.account, level = a.level));
} }
} }
}, },
@ -36,7 +37,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
match db.access_add(chan, account, &level) { match db.access_add(chan, account, &level) {
Ok(()) => ctx.notice(me, from.uid, format!("Added \x02{account}\x02 to \x02{chan}\x02 as \x02{level}\x02.")), 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)),
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."),
} }
} }
@ -49,8 +50,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
match db.access_del(chan, account) { match db.access_del(chan, account) {
Ok(true) => ctx.notice(me, from.uid, format!("Removed \x02{account}\x02 from \x02{chan}\x02.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Removed \x02{account}\x02 from \x02{chan}\x02.", account = account, chan = chan)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{account}\x02 has no access to \x02{chan}\x02.")), 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."),
} }
} }
@ -62,11 +63,11 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
fn is_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool { fn is_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool {
match db.channel(chan) { match db.channel(chan) {
None => { None => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
false false
} }
Some(info) if from.account != Some(info.founder.as_str()) => { Some(info) if from.account != Some(info.founder.as_str()) => {
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can change access.")); ctx.notice(me, from.uid, t!(ctx, "Only \x02{chan}\x02's founder can change access.", chan = chan));
false false
} }
// Founder, but a staff-suspended channel is frozen — no access changes. // Founder, but a staff-suspended channel is frozen — no access changes.

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST | CLEAR // AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST | CLEAR
// A mask is a nick!user@host glob or any extban the ircd advertises (named or by // A mask is a nick!user@host glob or any extban the ircd advertises (named or by
@ -14,12 +15,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() { match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() {
None | Some("LIST") => match db.channel(chan) { None | Some("LIST") => match db.channel(chan) {
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan)),
Some(info) if info.akick.is_empty() => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has an empty auto-kick list.")), Some(info) if info.akick.is_empty() => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has an empty auto-kick list.", chan = chan)),
Some(info) => { Some(info) => {
ctx.notice(me, from.uid, format!("Auto-kick list for \x02{}\x02:", info.name)); ctx.notice(me, from.uid, t!(ctx, "Auto-kick list for \x02{name}\x02:", name = info.name));
for k in &info.akick { for k in &info.akick {
ctx.notice(me, from.uid, format!(" \x02{}\x02 ({})", k.mask, k.reason)); ctx.notice(me, from.uid, t!(ctx, " \x02{mask}\x02 ({reason})", mask = k.mask, reason = k.reason));
} }
} }
}, },
@ -37,25 +38,25 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
echo_api::AkickMask::Host(_) => {} echo_api::AkickMask::Host(_) => {}
echo_api::AkickMask::Ext(eb, _) => { echo_api::AkickMask::Ext(eb, _) => {
if !db.extban_offered(eb.name) { if !db.extban_offered(eb.name) {
ctx.notice(me, from.uid, format!("This network's ircd doesn't offer the \x02{}\x02 extban.", eb.name)); ctx.notice(me, from.uid, t!(ctx, "This network's ircd doesn't offer the \x02{name}\x02 extban.", name = eb.name));
return; return;
} }
if !db.extban_enabled(eb.name) { if !db.extban_enabled(eb.name) {
ctx.notice(me, from.uid, format!("The \x02{}\x02 extban isn't enabled on this network.", eb.name)); ctx.notice(me, from.uid, t!(ctx, "The \x02{name}\x02 extban isn't enabled on this network.", name = eb.name));
return; return;
} }
} }
echo_api::AkickMask::Unknown(token) => match db.extban_lookup(token) { echo_api::AkickMask::Unknown(token) => match db.extban_lookup(token) {
None => { None => {
ctx.notice(me, from.uid, format!("\x02{token}\x02 isn't a host mask or an extban this network offers.")); ctx.notice(me, from.uid, t!(ctx, "\x02{token}\x02 isn't a host mask or an extban this network offers.", token = token));
return; return;
} }
Some(cap) if cap.acting => { Some(cap) if cap.acting => {
ctx.notice(me, from.uid, format!("\x02{}\x02 is an acting extban; auto-kick needs a matching extban or a host mask.", cap.name)); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 is an acting extban; auto-kick needs a matching extban or a host mask.", name = cap.name));
return; return;
} }
Some(cap) if !db.extban_enabled(&cap.name) => { Some(cap) if !db.extban_enabled(&cap.name) => {
ctx.notice(me, from.uid, format!("The \x02{}\x02 extban isn't enabled on this network.", cap.name)); ctx.notice(me, from.uid, t!(ctx, "The \x02{name}\x02 extban isn't enabled on this network.", name = cap.name));
return; return;
} }
Some(_) => {} Some(_) => {}
@ -73,7 +74,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if echo_api::ircd_enforced(mask) { if echo_api::ircd_enforced(mask) {
ctx.channel_mode(me, chan, &format!("+b {mask}")); ctx.channel_mode(me, chan, &format!("+b {mask}"));
} }
ctx.notice(me, from.uid, format!("Added \x02{mask}\x02 to \x02{chan}\x02's auto-kick list.")); ctx.notice(me, from.uid, t!(ctx, "Added \x02{mask}\x02 to \x02{chan}\x02's auto-kick list.", mask = mask, 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."),
} }
@ -92,9 +93,9 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if echo_api::ircd_enforced(mask) { if echo_api::ircd_enforced(mask) {
ctx.channel_mode(me, chan, &format!("-b {mask}")); ctx.channel_mode(me, chan, &format!("-b {mask}"));
} }
ctx.notice(me, from.uid, format!("Removed \x02{mask}\x02 from \x02{chan}\x02's auto-kick list.")); ctx.notice(me, from.uid, t!(ctx, "Removed \x02{mask}\x02 from \x02{chan}\x02's auto-kick list.", mask = mask, chan = chan));
} }
Ok(false) => ctx.notice(me, from.uid, format!("\x02{mask}\x02 isn't on \x02{chan}\x02's auto-kick list.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{mask}\x02 isn't on \x02{chan}\x02's auto-kick list.", mask = mask, 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."),
} }
} }
@ -110,7 +111,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
ctx.channel_mode(me, chan, &format!("-b {mask}")); ctx.channel_mode(me, chan, &format!("-b {mask}"));
} }
} }
ctx.notice(me, from.uid, format!("Cleared \x02{}\x02 entr{} from \x02{chan}\x02's auto-kick list.", masks.len(), if masks.len() == 1 { "y" } else { "ies" })); ctx.notice(me, from.uid, t!(ctx, "Cleared \x02{count}\x02 entr{suffix} from \x02{chan}\x02's auto-kick list.", count = masks.len(), suffix = if masks.len() == 1 { "y" } else { "ies" }, chan = chan));
} }
_ => ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST | CLEAR"), _ => ctx.notice(me, from.uid, "Syntax: AKICK <#channel> ADD <mask> [reason] | DEL <mask> | LIST | CLEAR"),
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// BAN <#channel> <nick> [reason]: ban *!*@host and kick the user. // BAN <#channel> <nick> [reason]: ban *!*@host and kick the user.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
@ -12,7 +13,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
return; return;
} }
let Some(target) = net.uid_by_nick(nick).map(str::to_string) else { let Some(target) = net.uid_by_nick(nick).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't here.")); ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 isn't here.", nick = nick));
return; return;
}; };
if super::peace_blocks(me, from, chan, &target, ctx, net, db) { if super::peace_blocks(me, from, chan, &target, ctx, net, db) {

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// CLONE <source> <target>: copy a channel's settings (mode lock, access, // CLONE <source> <target>: copy a channel's settings (mode lock, access,
// auto-kick, description, entry message) into another. Founder of both. // auto-kick, description, entry message) into another. Founder of both.
@ -34,5 +35,5 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if let Some(info) = db.channel(dest) { if let Some(info) = db.channel(dest) {
ctx.channel_mode(me, dest, &info.lock_modes()); ctx.channel_mode(me, dest, &info.lock_modes());
} }
ctx.notice(me, from.uid, format!("Copied \x02{src}\x02's settings to \x02{dest}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Copied \x02{src}\x02's settings to \x02{dest}\x02.", src = src, dest = dest));
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{status_mode, Sender, ServiceCtx}; use echo_api::{status_mode, Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// ENFORCE <#channel>: re-apply the channel's settings to everyone present — // ENFORCE <#channel>: re-apply the channel's settings to everyone present —
// the mode lock, access status modes, and the auto-kick list. // the mode lock, access status modes, and the auto-kick list.
@ -35,5 +36,5 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
} }
} }
} }
ctx.notice(me, from.uid, format!("Re-applied \x02{chan}\x02's settings to everyone present.")); ctx.notice(me, from.uid, t!(ctx, "Re-applied \x02{chan}\x02's settings to everyone present.", chan = chan));
} }

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// ENTRYMSG <#channel> [CLEAR | <text>]: message noticed to users as they join. // ENTRYMSG <#channel> [CLEAR | <text>]: message noticed to users as they join.
// With no argument, show the current message. // With no argument, show the current message.
@ -10,16 +11,16 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
match args.get(2) { match args.get(2) {
None => match db.channel(chan) { None => match db.channel(chan) {
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan)),
Some(info) if info.entrymsg.is_empty() => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no entry message.")), Some(info) if info.entrymsg.is_empty() => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has no entry message.", chan = chan)),
Some(info) => ctx.notice(me, from.uid, format!("Entry message for \x02{chan}\x02: {}", info.entrymsg)), Some(info) => ctx.notice(me, from.uid, t!(ctx, "Entry message for \x02{chan}\x02: {msg}", chan = chan, msg = info.entrymsg)),
}, },
Some(&kw) if kw.eq_ignore_ascii_case("CLEAR") => { Some(&kw) if kw.eq_ignore_ascii_case("CLEAR") => {
if !super::require_op(me, from, chan, ctx, db) { if !super::require_op(me, from, chan, ctx, db) {
return; return;
} }
match db.set_entrymsg(chan, "") { match db.set_entrymsg(chan, "") {
Ok(()) => ctx.notice(me, from.uid, format!("Entry message for \x02{chan}\x02 cleared.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Entry message for \x02{chan}\x02 cleared.", 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."),
} }
} }
@ -28,7 +29,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
match db.set_entrymsg(chan, &args[2..].join(" ")) { match db.set_entrymsg(chan, &args[2..].join(" ")) {
Ok(()) => ctx.notice(me, from.uid, format!("Entry message for \x02{chan}\x02 updated.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Entry message for \x02{chan}\x02 updated.", 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."),
} }
} }

View file

@ -1,4 +1,5 @@
use echo_api::{Flags, Sender, ServiceCtx, Store, ACCESS_FLAGS}; use echo_api::{Flags, Sender, ServiceCtx, Store, ACCESS_FLAGS};
use echo_api::t;
// FLAGS <#channel> [account [+/-flags]]: the granular access model. With no // FLAGS <#channel> [account [+/-flags]]: the granular access model. With no
// account, list the access entries and their flags; with an account, show or // account, list the access entries and their flags; with an account, show or
@ -8,7 +9,7 @@ use echo_api::{Flags, Sender, ServiceCtx, Store, ACCESS_FLAGS};
// 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, 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, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
}; };
let is_founder = from.account == Some(info.founder.as_str()); let is_founder = from.account == Some(info.founder.as_str());
@ -17,15 +18,15 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv
// FLAGS <#chan> — list. // FLAGS <#chan> — list.
let Some(&target) = args.get(2) else { let Some(&target) = args.get(2) else {
if !is_founder && !caps.op { if !is_founder && !caps.op {
ctx.notice(me, from.uid, format!("You need access to \x02{chan}\x02 to view its flags.")); ctx.notice(me, from.uid, t!(ctx, "You need access to \x02{chan}\x02 to view its flags.", chan = chan));
return; return;
} }
ctx.notice(me, from.uid, format!("Access flags for \x02{chan}\x02:")); ctx.notice(me, from.uid, t!(ctx, "Access flags for \x02{chan}\x02:", chan = chan));
ctx.notice(me, from.uid, format!(" \x02{}\x02 (founder): \x02f\x02", info.founder)); ctx.notice(me, from.uid, t!(ctx, " \x02{founder}\x02 (founder): \x02f\x02", founder = info.founder));
for a in &info.access { for a in &info.access {
ctx.notice(me, from.uid, format!(" \x02{}\x02: \x02{}\x02", a.account, Flags::from_level(&a.level).to_letters())); ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02: \x02{flags}\x02", account = a.account, flags = Flags::from_level(&a.level).to_letters()));
} }
ctx.notice(me, from.uid, format!("End of flags ({} entr{}).", info.access.len() + 1, if info.access.is_empty() { "y" } else { "ies" })); ctx.notice(me, from.uid, t!(ctx, "End of flags ({count} entr{suffix}).", count = info.access.len() + 1, suffix = if info.access.is_empty() { "y" } else { "ies" }));
return; return;
}; };
@ -34,15 +35,15 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv
// FLAGS <#chan> <account> — show one. // FLAGS <#chan> <account> — show one.
let Some(&delta) = args.get(3) else { let Some(&delta) = args.get(3) else {
match entry.as_deref().map(|l| Flags::from_level(l).to_letters()) { match entry.as_deref().map(|l| Flags::from_level(l).to_letters()) {
Some(f) if !f.is_empty() => ctx.notice(me, from.uid, format!("\x02{target}\x02 on \x02{chan}\x02: \x02{f}\x02")), Some(f) if !f.is_empty() => ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 on \x02{chan}\x02: \x02{flags}\x02", target = target, chan = chan, flags = f)),
_ => ctx.notice(me, from.uid, format!("\x02{target}\x02 has no access to \x02{chan}\x02.")), _ => ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 has no access to \x02{chan}\x02.", target = target, chan = chan)),
} }
return; return;
}; };
// FLAGS <#chan> <account> <+/-flags> — modify. // FLAGS <#chan> <account> <+/-flags> — modify.
if !is_founder && !caps.access { if !is_founder && !caps.access {
ctx.notice(me, from.uid, format!("You need the founder or the \x02a\x02 flag to change access on \x02{chan}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You need the founder or the \x02a\x02 flag to change access on \x02{chan}\x02.", chan = chan));
return; return;
} }
if super::suspended_block(me, from, chan, ctx, db) { if super::suspended_block(me, from, chan, ctx, db) {
@ -55,7 +56,7 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv
let updated = match Flags::from_level(entry.as_deref().unwrap_or("")).apply_delta(delta) { let updated = match Flags::from_level(entry.as_deref().unwrap_or("")).apply_delta(delta) {
Ok(f) => f, Ok(f) => f,
Err(bad) => { Err(bad) => {
ctx.notice(me, from.uid, format!("\x02{bad}\x02 isn't a valid flag. Valid flags: \x02{ACCESS_FLAGS}\x02.")); ctx.notice(me, from.uid, t!(ctx, "\x02{bad}\x02 isn't a valid flag. Valid flags: \x02{valid}\x02.", bad = bad, valid = ACCESS_FLAGS));
return; return;
} }
}; };
@ -63,19 +64,19 @@ pub fn handle(me: &str, from: &Sender, chan: &str, args: &[&str], ctx: &mut Serv
// `f` (founder/co-founder) — otherwise a delegate could mint a founder-equivalent // `f` (founder/co-founder) — otherwise a delegate could mint a founder-equivalent
// entry (Rank::Founder) and seize the channel. // entry (Rank::Founder) and seize the channel.
if !is_founder && updated.has(echo_api::Flag::Founder) { if !is_founder && updated.has(echo_api::Flag::Founder) {
ctx.notice(me, from.uid, format!("Only the founder can grant the \x02f\x02 flag on \x02{chan}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Only the founder can grant the \x02f\x02 flag on \x02{chan}\x02.", chan = chan));
return; return;
} }
if updated.is_empty() { if updated.is_empty() {
match db.access_del(chan, target) { match db.access_del(chan, target) {
Ok(true) => ctx.notice(me, from.uid, format!("Cleared \x02{target}\x02's access to \x02{chan}\x02.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Cleared \x02{target}\x02's access to \x02{chan}\x02.", target = target, chan = chan)),
_ => ctx.notice(me, from.uid, format!("\x02{target}\x02 had no access to \x02{chan}\x02.")), _ => ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 had no access to \x02{chan}\x02.", target = target, chan = chan)),
} }
return; return;
} }
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(()) => ctx.notice(me, from.uid, format!("\x02{target}\x02 on \x02{chan}\x02 now holds \x02{letters}\x02.")), 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)),
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."),
} }
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// GETKEY <#channel>: report the channel key (+k), for ops who need to let // GETKEY <#channel>: report the channel key (+k), for ops who need to let
// someone in. // someone in.
@ -13,7 +14,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
return; return;
} }
match net.channel_key(chan) { match net.channel_key(chan) {
Some(key) => ctx.notice(me, from.uid, format!("Key for \x02{chan}\x02 is \x02{key}\x02.")), Some(key) => ctx.notice(me, from.uid, t!(ctx, "Key for \x02{chan}\x02 is \x02{key}\x02.", chan = chan, key = key)),
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no key set.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has no key set.", chan = chan)),
} }
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// INVITE <#channel> [nick]: invite a user (self if no nick) into the channel. // INVITE <#channel> [nick]: invite a user (self if no nick) into the channel.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
@ -15,12 +16,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
Some(&nick) => match net.uid_by_nick(nick) { Some(&nick) => match net.uid_by_nick(nick) {
Some(u) => u.to_string(), Some(u) => u.to_string(),
None => { None => {
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't here.")); ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 isn't here.", nick = nick));
return; return;
} }
}, },
None => from.uid.to_string(), None => from.uid.to_string(),
}; };
ctx.invite(me, &target, chan); ctx.invite(me, &target, chan);
ctx.notice(me, from.uid, format!("Invited to \x02{chan}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Invited to \x02{chan}\x02.", chan = chan));
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// KICK <#channel> <nick> [reason]: kick a user. // KICK <#channel> <nick> [reason]: kick a user.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
@ -12,7 +13,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
return; return;
} }
let Some(target) = net.uid_by_nick(nick) else { let Some(target) = net.uid_by_nick(nick) else {
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't here.")); ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 isn't here.", nick = nick));
return; return;
}; };
if super::peace_blocks(me, from, chan, target, ctx, net, db) { if super::peace_blocks(me, from, chan, target, ctx, net, db) {

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{AccessRole, LevelCap, Sender, ServiceCtx}; use echo_api::{AccessRole, LevelCap, Sender, ServiceCtx};
use echo_api::t;
// LEVELS <#channel> [SET <capability> <tier> | RESET <capability>] // LEVELS <#channel> [SET <capability> <tier> | RESET <capability>]
// Tune which access tier holds each channel capability (OP, TOPIC, INVITE, ACCESS). // Tune which access tier holds each channel capability (OP, TOPIC, INVITE, ACCESS).
@ -20,16 +21,16 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
fn list(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) { fn list(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) {
let Some(info) = db.channel(chan) else { let Some(info) = db.channel(chan) else {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
}; };
ctx.notice(me, from.uid, format!("Access levels for \x02{}\x02 (capability → minimum tier):", info.name)); ctx.notice(me, from.uid, t!(ctx, "Access levels for \x02{name}\x02 (capability → minimum tier):", name = info.name));
for cap in LevelCap::ALL { for cap in LevelCap::ALL {
let (tier, tag) = match info.levels.iter().find(|(c, _)| *c == cap) { let (tier, tag) = match info.levels.iter().find(|(c, _)| *c == cap) {
Some((_, role)) => (*role, " (custom)"), Some((_, role)) => (*role, " (custom)"),
None => (cap.default_role(), ""), None => (cap.default_role(), ""),
}; };
ctx.notice(me, from.uid, format!(" \x02{}\x02{}{}", cap.name(), tier_word(tier), tag)); ctx.notice(me, from.uid, t!(ctx, " \x02{cap}\x02 — {tier}{tag}", cap = cap.name(), tier = tier_word(tier), tag = tag));
} }
ctx.notice(me, from.uid, "Founder: \x02LEVELS <#chan> SET <capability> <tier>\x02 grants it to that tier and above; \x02RESET\x02 restores the default."); ctx.notice(me, from.uid, "Founder: \x02LEVELS <#chan> SET <capability> <tier>\x02 grants it to that tier and above; \x02RESET\x02 restores the default.");
} }
@ -58,7 +59,7 @@ fn set(me: &str, from: &Sender, chan: &str, rest: &[&str], ctx: &mut ServiceCtx,
} }
}; };
match db.level_set(chan, cap.name(), tier_word(role)) { match db.level_set(chan, cap.name(), tier_word(role)) {
Ok(()) => ctx.notice(me, from.uid, format!("\x02{}\x02 on \x02{chan}\x02 is now held by \x02{}\x02 and above.", cap.name(), tier_word(role))), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "\x02{cap}\x02 on \x02{chan}\x02 is now held by \x02{tier}\x02 and above.", cap = cap.name(), chan = chan, tier = tier_word(role))),
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."),
} }
} }
@ -76,8 +77,8 @@ fn reset(me: &str, from: &Sender, chan: &str, cap_arg: Option<&str>, ctx: &mut S
return; return;
}; };
match db.level_reset(chan, cap.name()) { match db.level_reset(chan, cap.name()) {
Ok(true) => ctx.notice(me, from.uid, format!("\x02{}\x02 on \x02{chan}\x02 is back to its default tier (\x02{}\x02).", cap.name(), tier_word(cap.default_role()))), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "\x02{cap}\x02 on \x02{chan}\x02 is back to its default tier (\x02{tier}\x02).", cap = cap.name(), chan = chan, tier = tier_word(cap.default_role()))),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{}\x02 on \x02{chan}\x02 has no custom level.", cap.name())), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{cap}\x02 on \x02{chan}\x02 has no custom level.", cap = cap.name(), 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."),
} }
} }

View file

@ -1,6 +1,7 @@
use echo_api::{ChanError, ChannelView, ForbidKind, Priv, Store}; use echo_api::{ChanError, ChannelView, ForbidKind, Priv, Store};
use echo_api::{HelpEntry, Sender, Service, ServiceCtx}; use echo_api::{HelpEntry, Sender, Service, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
#[path = "mode.rs"] #[path = "mode.rs"]
mod mode; mod mode;
@ -141,11 +142,11 @@ impl Service for ChanServ {
}; };
// You can only register a channel you actually control right now. // You can only register a channel you actually control right now.
if !net.is_op(chan, from.uid) { if !net.is_op(chan, from.uid) {
ctx.notice(me, from.uid, format!("You must be a channel operator (\x02@\x02) in \x02{chan}\x02 to register it.")); ctx.notice(me, from.uid, t!(ctx, "You must be a channel operator (\x02@\x02) in \x02{chan}\x02 to register it.", chan = chan));
return; return;
} }
if let Some(reason) = db.is_forbidden(ForbidKind::Chan, chan) { if let Some(reason) = db.is_forbidden(ForbidKind::Chan, chan) {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 can't be registered: {reason}")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 can't be registered: {reason}", chan = chan, reason = reason));
return; return;
} }
// Refuse a look-alike / mixed-script channel name (e.g. a Cyrillic // Refuse a look-alike / mixed-script channel name (e.g. a Cyrillic
@ -161,15 +162,15 @@ impl Service for ChanServ {
Ok(()) => { Ok(()) => {
ctx.channel_mode(me, chan, "+r"); // mark the channel registered ctx.channel_mode(me, chan, "+r"); // mark the channel registered
ctx.count("chanserv.register"); ctx.count("chanserv.register");
ctx.notice(me, from.uid, format!("\x02{chan}\x02 is now registered and you are its founder. Enjoy!")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 is now registered and you are its founder. Enjoy!", chan = chan));
// Auto-assign the network's default bot, if one is set (BotServ AUTOASSIGN). // Auto-assign the network's default bot, if one is set (BotServ AUTOASSIGN).
if let Some(bot) = db.default_bot() { if let Some(bot) = db.default_bot() {
if db.assign_bot(chan, &bot).is_ok() { if db.assign_bot(chan, &bot).is_ok() {
ctx.notice(me, from.uid, format!("Assigned \x02{bot}\x02 to \x02{chan}\x02. Change it with \x02/msg BotServ ASSIGN\x02.")); ctx.notice(me, from.uid, t!(ctx, "Assigned \x02{bot}\x02 to \x02{chan}\x02. Change it with \x02/msg BotServ ASSIGN\x02.", bot = bot, chan = chan));
} }
} }
} }
Err(ChanError::Exists) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 is already registered. Try \x02INFO {chan}\x02 to see who owns it.")), Err(ChanError::Exists) => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 is already registered. Try \x02INFO {chan}\x02 to see who owns it.", 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."),
} }
} }
@ -180,20 +181,20 @@ impl Service for ChanServ {
}; };
match db.channel(chan) { match db.channel(chan) {
Some(info) => { Some(info) => {
ctx.notice(me, from.uid, format!("Information for \x02{}\x02:", info.name)); ctx.notice(me, from.uid, t!(ctx, "Information for \x02{name}\x02:", name = info.name));
ctx.notice(me, from.uid, format!(" Founder : \x02{}\x02", info.founder)); ctx.notice(me, from.uid, t!(ctx, " Founder : \x02{founder}\x02", founder = info.founder));
if !info.desc.is_empty() { if !info.desc.is_empty() {
ctx.notice(me, from.uid, format!(" Description: {}", info.desc)); ctx.notice(me, from.uid, t!(ctx, " Description: {desc}", desc = info.desc));
} }
if !info.url.is_empty() { if !info.url.is_empty() {
ctx.notice(me, from.uid, format!(" URL : {}", info.url)); ctx.notice(me, from.uid, t!(ctx, " URL : {url}", url = info.url));
} }
if !info.email.is_empty() { if !info.email.is_empty() {
ctx.notice(me, from.uid, format!(" Email : {}", info.email)); ctx.notice(me, from.uid, t!(ctx, " Email : {email}", email = info.email));
} }
ctx.notice(me, from.uid, format!(" Registered : {}", echo_api::human_time(info.ts))); ctx.notice(me, from.uid, t!(ctx, " Registered : {when}", when = echo_api::human_time(info.ts)));
if let Some(s) = db.channel_suspension(chan) { if let Some(s) = db.channel_suspension(chan) {
ctx.notice(me, from.uid, format!(" Suspended : by \x02{}\x02{}", s.by, s.reason)); ctx.notice(me, from.uid, t!(ctx, " Suspended : by \x02{by}\x02 — {reason}", by = s.by, reason = s.reason));
} }
let mut opts = Vec::new(); let mut opts = Vec::new();
if info.signkick { opts.push("SIGNKICK"); } if info.signkick { opts.push("SIGNKICK"); }
@ -203,16 +204,16 @@ impl Service for ChanServ {
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() {
ctx.notice(me, from.uid, format!(" Options : {}", opts.join(", "))); ctx.notice(me, from.uid, t!(ctx, " Options : {options}", options = opts.join(", ")));
} }
// A staff note is shown to operators only. // A staff note is shown to operators only.
if from.privs.has(Priv::Auspex) { if from.privs.has(Priv::Auspex) {
if let Some(note) = db.channel_note(chan) { if let Some(note) = db.channel_note(chan) {
ctx.notice(me, from.uid, format!(" Staff note : {note}")); ctx.notice(me, from.uid, t!(ctx, " Staff note : {note}", note = note));
} }
} }
} }
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan)),
} }
} }
Some("DROP") => { Some("DROP") => {
@ -224,12 +225,12 @@ impl Service for ChanServ {
let founder = match db.channel(chan) { let founder = match db.channel(chan) {
Some(info) => info.founder.clone(), Some(info) => info.founder.clone(),
None => { None => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
} }
}; };
if from.account != Some(founder.as_str()) { if from.account != Some(founder.as_str()) {
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can drop it.")); ctx.notice(me, from.uid, t!(ctx, "Only \x02{chan}\x02's founder can drop it.", chan = chan));
return; return;
} }
if suspended_block(me, from, chan, ctx, db) { if suspended_block(me, from, chan, ctx, db) {
@ -239,7 +240,7 @@ impl Service for ChanServ {
Ok(()) => { Ok(()) => {
ctx.channel_mode(me, chan, "-r"); // no longer registered ctx.channel_mode(me, chan, "-r"); // no longer registered
ctx.count("chanserv.drop"); ctx.count("chanserv.drop");
ctx.notice(me, from.uid, format!("\x02{chan}\x02 has been dropped and is no longer registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 has been dropped and is no longer registered.", 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."),
} }
@ -252,11 +253,11 @@ impl Service for ChanServ {
// No modes given: show the current lock. // No modes given: show the current lock.
if args.len() <= 2 { if args.len() <= 2 {
match db.channel(chan) { match db.channel(chan) {
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan)),
Some(info) if info.lock_on.is_empty() && info.lock_off.is_empty() => { Some(info) if info.lock_on.is_empty() && info.lock_off.is_empty() => {
ctx.notice(me, from.uid, format!("\x02{}\x02 has no mode lock set.", info.name)); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 has no mode lock set.", name = info.name));
} }
Some(info) => ctx.notice(me, from.uid, format!("Mode lock for \x02{}\x02: \x02{}\x02", info.name, show_mlock(&info))), Some(info) => ctx.notice(me, from.uid, t!(ctx, "Mode lock for \x02{name}\x02: \x02{lock}\x02", name = info.name, lock = show_mlock(&info))),
} }
return; return;
} }
@ -264,12 +265,12 @@ impl Service for ChanServ {
let founder = match db.channel(chan) { let founder = match db.channel(chan) {
Some(info) => info.founder.clone(), Some(info) => info.founder.clone(),
None => { None => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
} }
}; };
if from.account != Some(founder.as_str()) { if from.account != Some(founder.as_str()) {
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can set its mode lock.")); ctx.notice(me, from.uid, t!(ctx, "Only \x02{chan}\x02's founder can set its mode lock.", chan = chan));
return; return;
} }
if suspended_block(me, from, chan, ctx, db) { if suspended_block(me, from, chan, ctx, db) {
@ -281,7 +282,7 @@ impl Service for ChanServ {
if let Some(info) = db.channel(chan) { if let Some(info) = db.channel(chan) {
ctx.channel_mode(me, chan, &info.lock_modes()); // apply it now ctx.channel_mode(me, chan, &info.lock_modes()); // apply it now
} }
ctx.notice(me, from.uid, format!("Mode lock for \x02{chan}\x02 updated.")); ctx.notice(me, from.uid, t!(ctx, "Mode lock for \x02{chan}\x02 updated.", 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."),
} }
@ -334,7 +335,7 @@ impl Service for ChanServ {
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.
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "I don't know the command \x02{other}\x02. Try \x02HELP\x02.", other = other)),
None => {} None => {}
} }
} }
@ -347,12 +348,12 @@ fn require_founder(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db
} }
match db.channel(chan) { match db.channel(chan) {
None => { None => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
false false
} }
Some(info) if from.account == Some(info.founder.as_str()) => true, Some(info) if from.account == Some(info.founder.as_str()) => true,
_ => { _ => {
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can do that.")); ctx.notice(me, from.uid, t!(ctx, "Only \x02{chan}\x02's founder can do that.", chan = chan));
false false
} }
} }
@ -365,12 +366,12 @@ fn require_op(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dy
} }
match (from.account, db.channel(chan)) { match (from.account, db.channel(chan)) {
(_, None) => { (_, None) => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
false false
} }
(Some(acc), Some(info)) if info.is_op(acc) => true, (Some(acc), Some(info)) if info.is_op(acc) => true,
_ => { _ => {
ctx.notice(me, from.uid, format!("You need operator access to \x02{chan}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You need operator access to \x02{chan}\x02.", chan = chan));
false false
} }
} }
@ -379,7 +380,7 @@ fn require_op(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dy
// A suspended channel is frozen: ChanServ won't manage it (returns true + notices). // A suspended channel is frozen: ChanServ won't manage it (returns true + notices).
fn suspended_block(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool { fn suspended_block(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool {
if db.is_channel_suspended(chan) { if db.is_channel_suspended(chan) {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 is suspended by network staff and can't be managed right now.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 is suspended by network staff and can't be managed right now.", chan = chan));
return true; return true;
} }
false false

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// LIST: show all registered channels. // LIST: show all registered channels.
pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) {
@ -10,8 +11,8 @@ pub fn handle(me: &str, from: &Sender, _args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
names.sort_unstable(); names.sort_unstable();
ctx.notice(me, from.uid, format!("Registered channels ({}):", names.len())); ctx.notice(me, from.uid, t!(ctx, "Registered channels ({count}):", count = names.len()));
for n in names { for n in names {
ctx.notice(me, from.uid, format!(" \x02{n}\x02")); ctx.notice(me, from.uid, t!(ctx, " \x02{n}\x02", n = n));
} }
} }

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// MODE <#channel> <modes>: the founder sets channel modes via ChanServ. Extban // MODE <#channel> <modes>: the founder sets channel modes via ChanServ. Extban
// arguments on the ban-family list modes (+b/+e/+I) are held to the same // arguments on the ban-family list modes (+b/+e/+I) are held to the same
@ -16,12 +17,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
let founder = match db.channel(chan) { let founder = match db.channel(chan) {
Some(info) => info.founder.clone(), Some(info) => info.founder.clone(),
None => { None => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
} }
}; };
if from.account != Some(founder.as_str()) { if from.account != Some(founder.as_str()) {
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can change its modes.")); ctx.notice(me, from.uid, t!(ctx, "Only \x02{chan}\x02's founder can change its modes.", chan = chan));
return; return;
} }
if super::suspended_block(me, from, chan, ctx, db) { if super::suspended_block(me, from, chan, ctx, db) {
@ -32,18 +33,18 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
let core = mask.strip_prefix('!').unwrap_or(mask); // extbans may be inverted let core = mask.strip_prefix('!').unwrap_or(mask); // extbans may be inverted
if let echo_api::AkickMask::Ext(eb, _) = echo_api::AkickMask::parse(core) { if let echo_api::AkickMask::Ext(eb, _) = echo_api::AkickMask::parse(core) {
if !db.extban_offered(eb.name) { if !db.extban_offered(eb.name) {
ctx.notice(me, from.uid, format!("This network's ircd doesn't offer the \x02{}\x02 extban.", eb.name)); ctx.notice(me, from.uid, t!(ctx, "This network's ircd doesn't offer the \x02{name}\x02 extban.", name = eb.name));
return; return;
} }
if !db.extban_enabled(eb.name) { if !db.extban_enabled(eb.name) {
ctx.notice(me, from.uid, format!("The \x02{}\x02 extban isn't enabled on this network.", eb.name)); ctx.notice(me, from.uid, t!(ctx, "The \x02{name}\x02 extban isn't enabled on this network.", name = eb.name));
return; return;
} }
} }
} }
let modes = args[2..].join(" "); let modes = args[2..].join(" ");
ctx.channel_mode(me, chan, &modes); ctx.channel_mode(me, chan, &modes);
ctx.notice(me, from.uid, format!("Set \x02{modes}\x02 on \x02{chan}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Set \x02{modes}\x02 on \x02{chan}\x02.", modes = modes, chan = chan));
} }
// The parameters attached to +b/-b/+e/-e/+I/-I in a `<modes> [params...]` change. // The parameters attached to +b/-b/+e/-e/+I/-I in a `<modes> [params...]` change.

View file

@ -1,4 +1,5 @@
use echo_api::{Priv, Sender, ServiceCtx, Store}; use echo_api::{Priv, Sender, ServiceCtx, Store};
use echo_api::t;
// NOEXPIRE <#channel> {ON|OFF}: pin a channel so inactivity-expiry never drops // NOEXPIRE <#channel> {ON|OFF}: pin a channel so inactivity-expiry never drops
// it (or lift the pin). Oper-only (Priv::Admin). // it (or lift the pin). Oper-only (Priv::Admin).
@ -12,13 +13,13 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
if db.channel(chan).is_none() { if db.channel(chan).is_none() {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
} }
match db.set_channel_noexpire(chan, on) { match db.set_channel_noexpire(chan, on) {
Ok(true) if on => ctx.notice(me, from.uid, format!("\x02{chan}\x02 will no longer expire.")), Ok(true) if on => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 will no longer expire.", chan = chan)),
Ok(true) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 can expire from inactivity again.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 can expire from inactivity again.", chan = chan)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 was already set that way.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 was already set that way.", 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."),
} }
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// OP/DEOP/VOICE/DEVOICE <#channel> [nick]: set a status mode on a user (self if // OP/DEOP/VOICE/DEVOICE <#channel> [nick]: set a status mode on a user (self if
// no nick given). `mode` is the mode to apply, e.g. "+o". // no nick given). `mode` is the mode to apply, e.g. "+o".
@ -22,7 +23,7 @@ pub fn handle(me: &str, from: &Sender, mode: &str, args: &[&str], ctx: &mut Serv
Some(&nick) => match net.uid_by_nick(nick) { Some(&nick) => match net.uid_by_nick(nick) {
Some(u) => u.to_string(), Some(u) => u.to_string(),
None => { None => {
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't here.")); ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 isn't here.", nick = nick));
return; return;
} }
}, },

View file

@ -1,5 +1,6 @@
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// SEEN <nick> — network-wide: when a nick was last seen, and doing what. // SEEN <nick> — network-wide: when a nick was last seen, and doing what.
// SEEN <#channel> <nick> — channel-scoped: when last active there, and their last // SEEN <#channel> <nick> — channel-scoped: when last active there, and their last
@ -15,26 +16,26 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
// Present in THIS channel right now — online elsewhere doesn't count. // Present in THIS channel right now — online elsewhere doesn't count.
let here = net.uid_by_nick(nick).is_some_and(|uid| net.channel_members(chan).iter().any(|m| m == uid)); let here = net.uid_by_nick(nick).is_some_and(|uid| net.channel_members(chan).iter().any(|m| m == uid));
if here { if here {
ctx.notice(me, from.uid, format!("{}: \x02{nick}\x02 is here right now.", from.nick)); ctx.notice(me, from.uid, t!(ctx, "{who}: \x02{nick}\x02 is here right now.", who = from.nick, nick = nick));
return; return;
} }
match net.channel_seen(chan, nick) { match net.channel_seen(chan, nick) {
Some(s) => ctx.notice(me, from.uid, format!( Some(s) => ctx.notice(me, from.uid, t!(ctx,
"{}: \x02{}\x02 was last seen on \x02{chan}\x02 {}, last saying: {}", "{who}: \x02{nick}\x02 was last seen on \x02{chan}\x02 {when}, last saying: {msg}",
from.nick, s.nick, echo_api::human_time(s.ts), s.msg who = from.nick, nick = s.nick, chan = chan, when = echo_api::human_time(s.ts), msg = s.msg
)), )),
None => ctx.notice(me, from.uid, format!("{}: I have no record of \x02{nick}\x02 talking in \x02{chan}\x02.", from.nick)), None => ctx.notice(me, from.uid, t!(ctx, "{who}: I have no record of \x02{nick}\x02 talking in \x02{chan}\x02.", who = from.nick, nick = nick, chan = chan)),
} }
} }
// Network-wide. // Network-wide.
Some(&nick) => { Some(&nick) => {
if net.uid_by_nick(nick).is_some() { if net.uid_by_nick(nick).is_some() {
ctx.notice(me, from.uid, format!("\x02{nick}\x02 is currently online.")); ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 is currently online.", nick = nick));
return; return;
} }
match net.last_seen(nick) { match net.last_seen(nick) {
Some(s) => ctx.notice(me, from.uid, format!("\x02{}\x02 was last seen {} ({}).", s.nick, echo_api::human_time(s.ts), s.what)), Some(s) => ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 was last seen {when} ({what}).", nick = s.nick, when = echo_api::human_time(s.ts), what = s.what)),
None => ctx.notice(me, from.uid, format!("I have no record of \x02{nick}\x02.")), None => ctx.notice(me, from.uid, t!(ctx, "I have no record of \x02{nick}\x02.", nick = nick)),
} }
} }
None => ctx.notice(me, from.uid, "Syntax: SEEN <nick> | SEEN <#channel> <nick>"), None => ctx.notice(me, from.uid, "Syntax: SEEN <nick> | SEEN <#channel> <nick>"),

View file

@ -1,4 +1,5 @@
use echo_api::{ChanSetting, Sender, ServiceCtx, Store}; use echo_api::{ChanSetting, Sender, ServiceCtx, Store};
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, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -8,13 +9,13 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
let founder = match db.channel(chan) { let founder = match db.channel(chan) {
None => { None => {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
} }
Some(info) => info.founder.clone(), Some(info) => info.founder.clone(),
}; };
if from.account != Some(founder.as_str()) { if from.account != Some(founder.as_str()) {
ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can change its settings.")); ctx.notice(me, from.uid, t!(ctx, "Only \x02{chan}\x02's founder can change its settings.", chan = chan));
return; return;
} }
if super::suspended_block(me, from, chan, ctx, db) { if super::suspended_block(me, from, chan, ctx, db) {
@ -27,11 +28,11 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
if !db.exists(account) { if !db.exists(account) {
ctx.notice(me, from.uid, format!("\x02{account}\x02 isn't a registered account.")); ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 isn't a registered account.", account = account));
return; return;
} }
match db.set_founder(chan, account) { match db.set_founder(chan, account) {
Ok(()) => ctx.notice(me, from.uid, format!("Founder of \x02{chan}\x02 transferred to \x02{account}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Founder of \x02{chan}\x02 transferred to \x02{account}\x02.", chan = chan, account = account)),
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."),
} }
} }
@ -39,7 +40,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
match args.get(3) { match args.get(3) {
Some(&acct) if !acct.eq_ignore_ascii_case("OFF") => { Some(&acct) if !acct.eq_ignore_ascii_case("OFF") => {
if !db.exists(acct) { if !db.exists(acct) {
ctx.notice(me, from.uid, format!("\x02{acct}\x02 isn't a registered account.")); ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 isn't a registered account.", account = acct));
return; return;
} }
if acct.eq_ignore_ascii_case(&founder) { if acct.eq_ignore_ascii_case(&founder) {
@ -47,12 +48,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
match db.set_successor(chan, Some(acct)) { match db.set_successor(chan, Some(acct)) {
Ok(()) => ctx.notice(me, from.uid, format!("Successor of \x02{chan}\x02 set to \x02{acct}\x02. They inherit it if your account is dropped or expires.")), 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)),
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."),
} }
} }
_ => match db.set_successor(chan, None) { _ => match db.set_successor(chan, None) {
Ok(()) => ctx.notice(me, from.uid, format!("Successor of \x02{chan}\x02 cleared.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Successor of \x02{chan}\x02 cleared.", 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."),
}, },
} }
@ -60,23 +61,23 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
Some("DESC") => { Some("DESC") => {
let desc = if args.len() > 3 { args[3..].join(" ") } else { String::new() }; let desc = if args.len() > 3 { args[3..].join(" ") } else { String::new() };
match db.set_desc(chan, &desc) { match db.set_desc(chan, &desc) {
Ok(()) => ctx.notice(me, from.uid, format!("Description for \x02{chan}\x02 updated.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Description for \x02{chan}\x02 updated.", 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."),
} }
} }
Some("URL") => { Some("URL") => {
let url = if args.len() > 3 { args[3..].join(" ") } else { String::new() }; let url = if args.len() > 3 { args[3..].join(" ") } else { String::new() };
match db.set_url(chan, &url) { match db.set_url(chan, &url) {
Ok(()) if url.is_empty() => ctx.notice(me, from.uid, format!("URL for \x02{chan}\x02 cleared.")), Ok(()) if url.is_empty() => ctx.notice(me, from.uid, t!(ctx, "URL for \x02{chan}\x02 cleared.", chan = chan)),
Ok(()) => ctx.notice(me, from.uid, format!("URL for \x02{chan}\x02 updated.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "URL for \x02{chan}\x02 updated.", 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."),
} }
} }
Some("EMAIL") => { Some("EMAIL") => {
let email = args.get(3).copied().unwrap_or(""); let email = args.get(3).copied().unwrap_or("");
match db.set_channel_email(chan, email) { match db.set_channel_email(chan, email) {
Ok(()) if email.is_empty() => ctx.notice(me, from.uid, format!("Contact email for \x02{chan}\x02 cleared.")), Ok(()) if email.is_empty() => ctx.notice(me, from.uid, t!(ctx, "Contact email for \x02{chan}\x02 cleared.", chan = chan)),
Ok(()) => ctx.notice(me, from.uid, format!("Contact email for \x02{chan}\x02 updated.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Contact email for \x02{chan}\x02 updated.", 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."),
} }
} }
@ -115,12 +116,12 @@ fn toggle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store, cha
Some("ON") => true, Some("ON") => true,
Some("OFF") => false, Some("OFF") => false,
_ => { _ => {
ctx.notice(me, from.uid, format!("Syntax: SET <#channel> {name} {{ON|OFF}}")); ctx.notice(me, from.uid, t!(ctx, "Syntax: SET <#channel> {name} {ON|OFF}", name = name));
return; return;
} }
}; };
match db.set_channel_setting(chan, setting, on) { match db.set_channel_setting(chan, setting, on) {
Ok(()) => ctx.notice(me, from.uid, format!("\x02{name}\x02 for \x02{chan}\x02 is now \x02{}\x02.", if on { "on" } else { "off" })), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 for \x02{chan}\x02 is now \x02{state}\x02.", name = name, chan = chan, state = if on { "on" } else { "off" })),
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."),
} }
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{access_role, Sender, ServiceCtx}; use echo_api::{access_role, Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// STATUS <#channel> [nick]: show a user's access level (self if no nick). // STATUS <#channel> [nick]: show a user's access level (self if no nick).
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
@ -9,7 +10,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
return; return;
}; };
let Some(info) = db.channel(chan) else { let Some(info) = db.channel(chan) else {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
}; };
let (label, account) = match args.get(2) { let (label, account) = match args.get(2) {
@ -26,5 +27,5 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
None => "none", None => "none",
}, },
}; };
ctx.notice(me, from.uid, format!("\x02{label}\x02 access on \x02{chan}\x02: \x02{level}\x02")); ctx.notice(me, from.uid, t!(ctx, "\x02{label}\x02 access on \x02{chan}\x02: \x02{level}\x02", label = label, chan = chan, level = level));
} }

View file

@ -1,4 +1,5 @@
use echo_api::{parse_duration, NetView, Priv, Sender, ServiceCtx, Store}; use echo_api::{parse_duration, NetView, Priv, Sender, ServiceCtx, Store};
use echo_api::t;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
// SUSPEND <#channel> [+expiry] [reason] / UNSUSPEND <#channel>: freeze or unfreeze // SUSPEND <#channel> [+expiry] [reason] / UNSUSPEND <#channel>: freeze or unfreeze
@ -15,14 +16,14 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
return; return;
}; };
if db.channel(chan).is_none() { if db.channel(chan).is_none() {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
} }
if !suspending { if !suspending {
match db.unsuspend_channel(chan) { match db.unsuspend_channel(chan) {
Ok(true) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 is no longer suspended.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 is no longer suspended.", chan = chan)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't suspended.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't suspended.", 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."),
} }
return; return;
@ -42,7 +43,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
ctx.kick(me, chan, &uid, &reason); ctx.kick(me, chan, &uid, &reason);
} }
let expiry = if expires.is_some() { " (with expiry)" } else { "" }; let expiry = if expires.is_some() { " (with expiry)" } else { "" };
ctx.notice(me, from.uid, format!("\x02{chan}\x02 is now suspended{expiry}.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 is now suspended{expiry}.", chan = chan, expiry = expiry));
} }
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."),
} }

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// TOPIC <#channel> <text>: set the channel topic (empty clears it). // TOPIC <#channel> <text>: set the channel topic (empty clears it).
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -16,5 +17,5 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
// stored topic stays stale and KEEPTOPIC/TOPICLOCK restore the old one on // stored topic stays stale and KEEPTOPIC/TOPICLOCK restore the old one on
// recreation/restart. Ignore NoChannel (require_op already proved it exists). // recreation/restart. Ignore NoChannel (require_op already proved it exists).
let _ = db.set_channel_topic(chan, &text); let _ = db.set_channel_topic(chan, &text);
ctx.notice(me, from.uid, format!("Topic for \x02{chan}\x02 updated.")); ctx.notice(me, from.uid, t!(ctx, "Topic for \x02{chan}\x02 updated.", chan = chan));
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// UNBAN <#channel> [nick]: remove the *!*@host ban of a user (self if no nick). // UNBAN <#channel> [nick]: remove the *!*@host ban of a user (self if no nick).
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
@ -18,5 +19,5 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
.unwrap_or_else(|| from.uid.to_string()); .unwrap_or_else(|| from.uid.to_string());
let host = net.host_of(&target).unwrap_or("*"); let host = net.host_of(&target).unwrap_or("*");
ctx.channel_mode(me, chan, &format!("-b *!*@{host}")); ctx.channel_mode(me, chan, &format!("-b *!*@{host}"));
ctx.notice(me, from.uid, format!("Cleared the *!*@{host} ban on \x02{chan}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Cleared the *!*@{host} ban on \x02{chan}\x02.", host = host, chan = chan));
} }

View file

@ -1,4 +1,5 @@
use echo_api::{status_mode, NetView, Sender, ServiceCtx, Store}; use echo_api::{status_mode, NetView, Sender, ServiceCtx, Store};
use echo_api::t;
// UP <#channel>: (re)apply the status mode your access entitles you to. // UP <#channel>: (re)apply the status mode your access entitles you to.
// DOWN <#channel>: drop your channel status modes. `up` selects which. // DOWN <#channel>: drop your channel status modes. `up` selects which.
@ -12,24 +13,24 @@ pub fn handle(me: &str, from: &Sender, up: bool, args: &[&str], ctx: &mut Servic
return; return;
}; };
let Some(info) = db.channel(chan) else { let Some(info) = db.channel(chan) else {
ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan));
return; return;
}; };
if !net.channel_members(chan).iter().any(|u| u.as_str() == from.uid) { if !net.channel_members(chan).iter().any(|u| u.as_str() == from.uid) {
ctx.notice(me, from.uid, format!("You're not in \x02{chan}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You're not in \x02{chan}\x02.", chan = chan));
return; return;
} }
if up { if up {
match info.join_mode(account) { match info.join_mode(account) {
Some(mode) => { Some(mode) => {
ctx.channel_mode(me, chan, &status_mode(mode, from.uid)); ctx.channel_mode(me, chan, &status_mode(mode, from.uid));
ctx.notice(me, from.uid, format!("Your status in \x02{chan}\x02 has been applied.")); ctx.notice(me, from.uid, t!(ctx, "Your status in \x02{chan}\x02 has been applied.", chan = chan));
} }
None => ctx.notice(me, from.uid, format!("You have no status access in \x02{chan}\x02.")), None => ctx.notice(me, from.uid, t!(ctx, "You have no status access in \x02{chan}\x02.", chan = chan)),
} }
} else { } else {
// Strip owner, admin, op, halfop and voice; the ircd ignores any you don't hold. // Strip owner, admin, op, halfop and voice; the ircd ignores any you don't hold.
ctx.channel_mode(me, chan, &status_mode("-qaohv", from.uid)); ctx.channel_mode(me, chan, &status_mode("-qaohv", from.uid));
ctx.notice(me, from.uid, format!("Your status in \x02{chan}\x02 has been removed.")); ctx.notice(me, from.uid, t!(ctx, "Your status in \x02{chan}\x02 has been removed.", chan = chan));
} }
} }

View file

@ -1,52 +1,53 @@
use echo_api::{access_role, Sender, ServiceCtx, Store}; use echo_api::{access_role, Sender, ServiceCtx, Store};
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").
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, db: &mut dyn Store) {
let Some(&chan) = args.get(1) else { let Some(&chan) = args.get(1) else {
ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST")); ctx.notice(me, from.uid, t!(ctx, "Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST", word = word));
return; return;
}; };
match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() { match args.get(2).map(|s| s.to_ascii_uppercase()).as_deref() {
None | Some("LIST") => match db.channel(chan) { None | Some("LIST") => match db.channel(chan) {
None => ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{chan}\x02 isn't registered.", chan = chan)),
Some(info) => { Some(info) => {
ctx.notice(me, from.uid, format!("{word} list for \x02{}\x02:", info.name)); ctx.notice(me, from.uid, t!(ctx, "{word} list for \x02{name}\x02:", word = word, name = info.name));
// List by resolved tier, not the raw level string, so an entry set // List by resolved tier, not the raw level string, so an entry set
// via granular FLAGS appears under the same tier a XOP ADD would. // via granular FLAGS appears under the same tier a XOP ADD would.
for a in info.access.iter().filter(|a| access_role(&a.level).xop_word() == Some(word)) { for a in info.access.iter().filter(|a| access_role(&a.level).xop_word() == Some(word)) {
ctx.notice(me, from.uid, format!(" \x02{}\x02", a.account)); ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02", account = a.account));
} }
} }
}, },
Some("ADD") => { Some("ADD") => {
let Some(&account) = args.get(3) else { let Some(&account) = args.get(3) else {
ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD <account>")); ctx.notice(me, from.uid, t!(ctx, "Syntax: {word} <#channel> ADD <account>", word = word));
return; return;
}; };
if !super::require_founder(me, from, chan, ctx, db) { if !super::require_founder(me, from, chan, ctx, db) {
return; return;
} }
match db.access_add(chan, account, level) { match db.access_add(chan, account, level) {
Ok(()) => ctx.notice(me, from.uid, format!("Added \x02{account}\x02 to \x02{chan}\x02's {word} list.")), 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)),
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("DEL") => { Some("DEL") => {
let Some(&account) = args.get(3) else { let Some(&account) = args.get(3) else {
ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> DEL <account>")); ctx.notice(me, from.uid, t!(ctx, "Syntax: {word} <#channel> DEL <account>", word = word));
return; return;
}; };
if !super::require_founder(me, from, chan, ctx, db) { if !super::require_founder(me, from, chan, ctx, db) {
return; return;
} }
match db.access_del(chan, account) { match db.access_del(chan, account) {
Ok(true) => ctx.notice(me, from.uid, format!("Removed \x02{account}\x02 from \x02{chan}\x02's {word} list.")), 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)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{account}\x02 has no access to \x02{chan}\x02.")), 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."),
} }
} }
_ => ctx.notice(me, from.uid, format!("Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST")), _ => ctx.notice(me, from.uid, t!(ctx, "Syntax: {word} <#channel> ADD <account> | DEL <account> | LIST", word = word)),
} }
} }

View file

@ -10,7 +10,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use echo_api::{HelpEntry, NetAction, NetView, Sender, Service, ServiceCtx, Store}; use echo_api::{t, HelpEntry, NetAction, NetView, Sender, Service, ServiceCtx, Store};
#[path = "chess.rs"] #[path = "chess.rs"]
mod chess; mod chess;

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// ADD <!group> <account>: add a plain member (no flags). Needs founder or `f`. // ADD <!group> <account>: add a plain member (no flags). Needs founder or `f`.
pub fn handle(me: &str, from: &Sender, name: Option<&str>, target: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, name: Option<&str>, target: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -8,19 +8,19 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, target: Option<&str>,
return; return;
}; };
let Some(g) = db.group(name) else { let Some(g) = db.group(name) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 isn't registered.", name = name));
return; return;
}; };
if !super::can_manage(&g, acc) { if !super::can_manage(&g, acc) {
ctx.notice(me, from.uid, format!("You need the founder or the \x02f\x02 flag to manage \x02{name}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You need the founder or the \x02f\x02 flag to manage \x02{name}\x02.", name = name));
return; return;
} }
let Some(canonical) = db.resolve_account(target).map(str::to_string) else { let Some(canonical) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't registered.", target = target));
return; return;
}; };
match db.group_set_flags(name, &canonical, "") { match db.group_set_flags(name, &canonical, "") {
Ok(()) => ctx.notice(me, from.uid, format!("Added \x02{canonical}\x02 to \x02{name}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Added \x02{canonical}\x02 to \x02{name}\x02.", canonical = canonical, name = name)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// DEL <!group> <account>: remove a member. Needs founder or the `f` flag. // DEL <!group> <account>: remove a member. Needs founder or the `f` flag.
pub fn handle(me: &str, from: &Sender, name: Option<&str>, target: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, name: Option<&str>, target: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -8,16 +8,16 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, target: Option<&str>,
return; return;
}; };
let Some(g) = db.group(name) else { let Some(g) = db.group(name) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 isn't registered.", name = name));
return; return;
}; };
if !super::can_manage(&g, acc) { if !super::can_manage(&g, acc) {
ctx.notice(me, from.uid, format!("You need the founder or the \x02f\x02 flag to manage \x02{name}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You need the founder or the \x02f\x02 flag to manage \x02{name}\x02.", name = name));
return; return;
} }
match db.group_del_member(name, target) { match db.group_del_member(name, target) {
Ok(true) => ctx.notice(me, from.uid, format!("Removed \x02{target}\x02 from \x02{name}\x02.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Removed \x02{target}\x02 from \x02{name}\x02.", target = target, name = name)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't in \x02{name}\x02.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't in \x02{name}\x02.", target = target, name = name)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// DROP <!group>: delete a group. Founder only. // DROP <!group>: delete a group. Founder only.
pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -8,15 +8,15 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx,
return; return;
}; };
let Some(g) = db.group(name) else { let Some(g) = db.group(name) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 isn't registered.", name = name));
return; return;
}; };
if !g.founder.eq_ignore_ascii_case(acc) { if !g.founder.eq_ignore_ascii_case(acc) {
ctx.notice(me, from.uid, format!("Only \x02{name}\x02's founder can drop it.")); ctx.notice(me, from.uid, t!(ctx, "Only \x02{name}\x02's founder can drop it.", name = name));
return; return;
} }
match db.group_drop(name) { match db.group_drop(name) {
Ok(()) => ctx.notice(me, from.uid, format!("Group \x02{name}\x02 has been dropped.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Group \x02{name}\x02 has been dropped.", name = name)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{GroupFlags, Sender, ServiceCtx, Store, GROUP_FLAGS}; use echo_api::{t, GroupFlags, Sender, ServiceCtx, Store, GROUP_FLAGS};
// FLAGS <!group> [account [+/-flags]]: list, show, or change group-access flags. // FLAGS <!group> [account [+/-flags]]: list, show, or change group-access flags.
// Listing/showing is open; changing needs the founder or the `f` flag. // Listing/showing is open; changing needs the founder or the `f` flag.
@ -9,15 +9,15 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, target: Option<&str>,
return; return;
}; };
let Some(g) = db.group(name) else { let Some(g) = db.group(name) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 isn't registered.", name = name));
return; return;
}; };
// List. // List.
let Some(target) = target else { let Some(target) = target else {
ctx.notice(me, from.uid, format!("Flags for \x02{}\x02:", g.name)); ctx.notice(me, from.uid, t!(ctx, "Flags for \x02{name}\x02:", name = g.name));
ctx.notice(me, from.uid, format!(" \x02{}\x02 (founder): \x02F\x02", g.founder)); ctx.notice(me, from.uid, t!(ctx, " \x02{founder}\x02 (founder): \x02F\x02", founder = g.founder));
for m in &g.members { for m in &g.members {
ctx.notice(me, from.uid, format!(" \x02{}\x02: \x02{}\x02", m.account, if m.flags.is_empty() { "(member)" } else { &m.flags })); ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02: \x02{flags}\x02", account = m.account, flags = if m.flags.is_empty() { "(member)" } else { &m.flags }));
} }
return; return;
}; };
@ -25,30 +25,30 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, target: Option<&str>,
// Show one. // Show one.
let Some(delta) = delta else { let Some(delta) = delta else {
match current { match current {
Some(f) => ctx.notice(me, from.uid, format!("\x02{target}\x02 in \x02{name}\x02: \x02{}\x02", if f.is_empty() { "(member)" } else { &f })), Some(f) => ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 in \x02{name}\x02: \x02{flags}\x02", target = target, name = name, flags = if f.is_empty() { "(member)" } else { &f })),
None => ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't in \x02{name}\x02.")), None => ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't in \x02{name}\x02.", target = target, name = name)),
} }
return; return;
}; };
// Change — needs founder or f flag. // Change — needs founder or f flag.
if !super::can_manage(&g, acc) { if !super::can_manage(&g, acc) {
ctx.notice(me, from.uid, format!("You need the founder or the \x02f\x02 flag to change \x02{name}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You need the founder or the \x02f\x02 flag to change \x02{name}\x02.", name = name));
return; return;
} }
let Some(canonical) = db.resolve_account(target).map(str::to_string) else { let Some(canonical) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't registered.", target = target));
return; return;
}; };
let updated = match GroupFlags::parse(current.as_deref().unwrap_or("")).apply_delta(delta) { let updated = match GroupFlags::parse(current.as_deref().unwrap_or("")).apply_delta(delta) {
Ok(f) => f, Ok(f) => f,
Err(bad) => { Err(bad) => {
ctx.notice(me, from.uid, format!("\x02{bad}\x02 isn't a valid group flag. Valid: \x02{GROUP_FLAGS}\x02.")); ctx.notice(me, from.uid, t!(ctx, "\x02{bad}\x02 isn't a valid group flag. Valid: \x02{valid}\x02.", bad = bad, valid = GROUP_FLAGS));
return; return;
} }
}; };
let letters = updated.to_letters(); let letters = updated.to_letters();
match db.group_set_flags(name, &canonical, &letters) { match db.group_set_flags(name, &canonical, &letters) {
Ok(()) => ctx.notice(me, from.uid, format!("\x02{canonical}\x02 in \x02{name}\x02 now holds \x02{}\x02.", if letters.is_empty() { "(member)" } else { &letters })), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "\x02{canonical}\x02 in \x02{name}\x02 now holds \x02{flags}\x02.", canonical = canonical, name = name, flags = if letters.is_empty() { "(member)" } else { &letters })),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// INFO <!group>: show a group's founder and member count. // INFO <!group>: show a group's founder and member count.
pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -7,10 +7,10 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx,
return; return;
}; };
let Some(g) = db.group(name) else { let Some(g) = db.group(name) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 isn't registered.", name = name));
return; return;
}; };
ctx.notice(me, from.uid, format!("Information for \x02{}\x02:", g.name)); ctx.notice(me, from.uid, t!(ctx, "Information for \x02{name}\x02:", name = g.name));
ctx.notice(me, from.uid, format!(" Founder : \x02{}\x02", g.founder)); ctx.notice(me, from.uid, t!(ctx, " Founder : \x02{founder}\x02", founder = g.founder));
ctx.notice(me, from.uid, format!(" Members : {}", g.members.len())); ctx.notice(me, from.uid, t!(ctx, " Members : {count}", count = g.members.len()));
} }

View file

@ -8,7 +8,7 @@
//! `lib.rs` holds the dispatcher and the two shared guards; each command lives //! `lib.rs` holds the dispatcher and the two shared guards; each command lives
//! in its own file. //! in its own file.
use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; use echo_api::{t, HelpEntry, NetView, Sender, Service, ServiceCtx, Store};
#[path = "register.rs"] #[path = "register.rs"]
mod register; mod register;
@ -67,7 +67,7 @@ impl Service for GroupServ {
Some("DEL") => del::handle(me, from, args.get(1).copied(), args.get(2).copied(), ctx, db), Some("DEL") => del::handle(me, from, args.get(1).copied(), args.get(2).copied(), ctx, db),
Some("FLAGS") => flags::handle(me, from, args.get(1).copied(), args.get(2).copied(), args.get(3).copied(), ctx, db), Some("FLAGS") => flags::handle(me, from, args.get(1).copied(), args.get(2).copied(), args.get(3).copied(), ctx, db),
Some("HELP") | None => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()), Some("HELP") | None => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()),
Some(other) => ctx.notice(me, from.uid, format!("I don't know \x02{other}\x02. Try \x02HELP\x02.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "I don't know \x02{other}\x02. Try \x02HELP\x02.", other = other)),
} }
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// LIST: operators see every group; others see the ones they belong to. // LIST: operators see every group; others see the ones they belong to.
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -14,7 +14,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store)
return; return;
} }
for n in &names { for n in &names {
ctx.notice(me, from.uid, format!(" \x02{n}\x02")); ctx.notice(me, from.uid, t!(ctx, " \x02{n}\x02", n = n));
} }
ctx.notice(me, from.uid, format!("End of list ({} group(s)).", names.len())); ctx.notice(me, from.uid, t!(ctx, "End of list ({count} group(s)).", count = names.len()));
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// REGISTER <!group>: create a group with you as its founder. // REGISTER <!group>: create a group with you as its founder.
pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -29,13 +29,13 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx,
// (mirrors NickServ's grouped-nick and AJOIN caps). // (mirrors NickServ's grouped-nick and AJOIN caps).
const MAX_GROUPS: usize = 25; const MAX_GROUPS: usize = 25;
if db.groups_founded(acc) >= MAX_GROUPS { if db.groups_founded(acc) >= MAX_GROUPS {
ctx.notice(me, from.uid, format!("You've reached the maximum of {MAX_GROUPS} registered groups.")); ctx.notice(me, from.uid, t!(ctx, "You've reached the maximum of {max} registered groups.", max = MAX_GROUPS));
return; return;
} }
let acc = acc.to_string(); let acc = acc.to_string();
match db.group_register(name, &acc) { match db.group_register(name, &acc) {
Ok(()) => ctx.notice(me, from.uid, format!("Group \x02{name}\x02 registered — you're the founder.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Group \x02{name}\x02 registered — you're the founder.", name = name)),
Err(echo_api::ChanError::Exists) => ctx.notice(me, from.uid, format!("\x02{name}\x02 is already registered.")), Err(echo_api::ChanError::Exists) => ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 is already registered.", name = name)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// CANCEL: withdraw your own newest open ticket. // CANCEL: withdraw your own newest open ticket.
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -15,7 +15,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store)
match mine { match mine {
Some(t) => { Some(t) => {
db.help_close(t.id); db.help_close(t.id);
ctx.notice(me, from.uid, format!("Your request \x02#{}\x02 has been cancelled.", t.id)); ctx.notice(me, from.uid, t!(ctx, "Your request \x02#{id}\x02 has been cancelled.", id = t.id));
} }
None => ctx.notice(me, from.uid, "You have no open request to cancel."), None => ctx.notice(me, from.uid, "You have no open request to cancel."),
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// CLOSE <id> (aka RESOLVE): resolve a ticket. // CLOSE <id> (aka RESOLVE): resolve a ticket.
pub fn handle(me: &str, from: &Sender, id: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, id: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -10,8 +10,8 @@ pub fn handle(me: &str, from: &Sender, id: Option<&str>, ctx: &mut ServiceCtx, d
return; return;
}; };
if db.help_close(n) { if db.help_close(n) {
ctx.notice(me, from.uid, format!("Ticket \x02#{n}\x02 closed.")); ctx.notice(me, from.uid, t!(ctx, "Ticket \x02#{id}\x02 closed.", id = n));
} else { } else {
ctx.notice(me, from.uid, format!("Ticket \x02#{n}\x02 isn't open (or doesn't exist).")); ctx.notice(me, from.uid, t!(ctx, "Ticket \x02#{id}\x02 isn't open (or doesn't exist).", id = n));
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// A friendly one-liner (with an example) for each extban, keyed by its ircd name. // A friendly one-liner (with an example) for each extban, keyed by its ircd name.
// The letter and whether it's offered come live from the ircd's CAPAB set, so this // The letter and whether it's offered come live from the ircd's CAPAB set, so this
@ -53,8 +53,14 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
let line = |ctx: &mut ServiceCtx, e: &echo_api::ExtbanCap| { let line = |ctx: &mut ServiceCtx, e: &echo_api::ExtbanCap| {
let letter = e.letter.map_or_else(|| "-".to_string(), |c| c.to_string()); let letter = e.letter.map_or_else(|| "-".to_string(), |c| c.to_string());
match DESC.iter().find(|(n, _)| n.eq_ignore_ascii_case(&e.name)) { match DESC.iter().find(|(n, _)| n.eq_ignore_ascii_case(&e.name)) {
Some((_, desc)) => say(ctx, format!(" \x02{}\x02 (\x02{}\x02) — {}", e.name, letter, desc)), Some((_, desc)) => {
None => say(ctx, format!(" \x02{}\x02 (\x02{}\x02)", e.name, letter)), let s = t!(ctx, " \x02{name}\x02 (\x02{letter}\x02) — {desc}", name = e.name, letter = letter, desc = desc);
say(ctx, s)
}
None => {
let s = t!(ctx, " \x02{name}\x02 (\x02{letter}\x02)", name = e.name, letter = letter);
say(ctx, s)
}
} }
}; };
@ -62,7 +68,8 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
// We haven't learned the ircd's set yet — show the reference from DESC. // We haven't learned the ircd's set yet — show the reference from DESC.
say(ctx, "\x02Extban types\x02 (your network's exact set is confirmed on link):".to_string()); say(ctx, "\x02Extban types\x02 (your network's exact set is confirmed on link):".to_string());
for (name, desc) in DESC { for (name, desc) in DESC {
say(ctx, format!(" \x02{name}\x02{desc}")); let s = t!(ctx, " \x02{name}\x02 — {desc}", name = name, desc = desc);
say(ctx, s);
} }
return; return;
} }

View file

@ -7,7 +7,7 @@
//! `lib.rs` holds the dispatcher and the shared guard/claim helpers; each //! `lib.rs` holds the dispatcher and the shared guard/claim helpers; each
//! command lives in its own file. //! command lives in its own file.
use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; use echo_api::{t, HelpEntry, NetView, Sender, Service, ServiceCtx, Store};
#[path = "request.rs"] #[path = "request.rs"]
mod request; mod request;
@ -78,7 +78,7 @@ impl Service for HelpServ {
None => { None => {
echo_api::help(me, from, ctx, BLURB, TOPICS, other); echo_api::help(me, from, ctx, BLURB, TOPICS, other);
if other.is_none() { if other.is_none() {
ctx.notice(me, from.uid, format!("For a service's own commands, type \x02HELP <service>\x02. Services: {}.", net.help_services().join(", "))); ctx.notice(me, from.uid, t!(ctx, "For a service's own commands, type \x02HELP <service>\x02. Services: {services}.", services = net.help_services().join(", ")));
ctx.notice(me, from.uid, "For the extended ban types you can use, type \x02HELP EXTBANS\x02."); ctx.notice(me, from.uid, "For the extended ban types you can use, type \x02HELP EXTBANS\x02.");
} }
} }
@ -87,7 +87,7 @@ impl Service for HelpServ {
// Not a HelpServ command — maybe the name of a service to get help for. // Not a HelpServ command — maybe the name of a service to get help for.
Some(_) => match net.service_help(args[0]) { Some(_) => match net.service_help(args[0]) {
Some((blurb, topics)) => echo_api::help(me, from, ctx, blurb, topics, args.get(1).copied()), Some((blurb, topics)) => echo_api::help(me, from, ctx, blurb, topics, args.get(1).copied()),
None => ctx.notice(me, from.uid, format!("I don't know \x02{}\x02. Try \x02REQUEST\x02 <message>, \x02HELP\x02, or a service name (e.g. \x02NickServ\x02).", args[0])), None => ctx.notice(me, from.uid, t!(ctx, "I don't know \x02{name}\x02. Try \x02REQUEST\x02 <message>, \x02HELP\x02, or a service name (e.g. \x02NickServ\x02).", name = args[0])),
}, },
} }
} }
@ -103,8 +103,8 @@ fn take_id(me: &str, from: &Sender, id: u64, ctx: &mut ServiceCtx, db: &mut dyn
let handler = from.account.unwrap_or(from.nick); let handler = from.account.unwrap_or(from.nick);
if db.help_take(id, handler) { if db.help_take(id, handler) {
let msg = db.help_ticket(id).map(|t| t.message).unwrap_or_default(); let msg = db.help_ticket(id).map(|t| t.message).unwrap_or_default();
ctx.notice(me, from.uid, format!("You took ticket \x02#{id}\x02: {msg}")); ctx.notice(me, from.uid, t!(ctx, "You took ticket \x02#{id}\x02: {msg}", id = id, msg = msg));
} else { } else {
ctx.notice(me, from.uid, format!("Ticket \x02#{id}\x02 isn't open (or doesn't exist).")); ctx.notice(me, from.uid, t!(ctx, "Ticket \x02#{id}\x02 isn't open (or doesn't exist).", id = id));
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// LIST [ALL]: operators list the open queue (or every ticket with ALL). // LIST [ALL]: operators list the open queue (or every ticket with ALL).
pub fn handle(me: &str, from: &Sender, arg: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, arg: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -13,12 +13,12 @@ pub fn handle(me: &str, from: &Sender, arg: Option<&str>, ctx: &mut ServiceCtx,
} }
for t in &tickets { for t in &tickets {
let state = match (&t.handler, t.open) { let state = match (&t.handler, t.open) {
(_, false) => " (closed)".to_string(), (_, false) => t!(ctx, " (closed)"),
(Some(h), true) => format!(" (taken by {h})"), (Some(h), true) => t!(ctx, " (taken by {handler})", handler = h),
(None, true) => String::new(), (None, true) => String::new(),
}; };
let short: String = t.message.chars().take(60).collect(); let short: String = t.message.chars().take(60).collect();
ctx.notice(me, from.uid, format!("\x02#{}\x02 {}{}{}", t.id, t.requester, short, state)); ctx.notice(me, from.uid, t!(ctx, "\x02#{id}\x02 {requester} — {short}{state}", id = t.id, requester = t.requester, short = short, state = state));
} }
ctx.notice(me, from.uid, format!("{} ticket(s). \x02VIEW\x02 <id> for detail.", tickets.len())); ctx.notice(me, from.uid, t!(ctx, "{count} ticket(s). \x02VIEW\x02 <id> for detail.", count = tickets.len()));
} }

View file

@ -1,4 +1,4 @@
use echo_api::{NetView, Sender, ServiceCtx, Store}; use echo_api::{t, NetView, Sender, ServiceCtx, Store};
// REQUEST <message> (aka HELPME): open a help-desk ticket for the staff. // REQUEST <message> (aka HELPME): open a help-desk ticket for the staff.
pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) {
@ -11,7 +11,7 @@ pub fn handle(me: &str, from: &Sender, rest: &[&str], ctx: &mut ServiceCtx, net:
// Rate-limit on the real host, not the spoofable nick (see REPORT). // Rate-limit on the real host, not the spoofable nick (see REPORT).
let cooldown_key = net.host_of(from.uid).unwrap_or(from.uid); let cooldown_key = net.host_of(from.uid).unwrap_or(from.uid);
match db.help_request(requester, cooldown_key, &message) { match db.help_request(requester, cooldown_key, &message) {
Some(id) => ctx.notice(me, from.uid, format!("Thanks — your request (\x02#{id}\x02) is in the queue. A staff member will be with you.")), Some(id) => ctx.notice(me, from.uid, t!(ctx, "Thanks — your request (\x02#{id}\x02) is in the queue. A staff member will be with you.", id = id)),
None => ctx.notice(me, from.uid, "You just opened a ticket — please wait a moment before opening another."), None => ctx.notice(me, from.uid, "You just opened a ticket — please wait a moment before opening another."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{human_time, Sender, ServiceCtx, Store}; use echo_api::{human_time, t, Sender, ServiceCtx, Store};
// VIEW <id> (aka READ): operators read a ticket in full. // VIEW <id> (aka READ): operators read a ticket in full.
pub fn handle(me: &str, from: &Sender, id: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, id: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -9,12 +9,12 @@ pub fn handle(me: &str, from: &Sender, id: Option<&str>, ctx: &mut ServiceCtx, d
ctx.notice(me, from.uid, "No such ticket. Syntax: VIEW <id>"); ctx.notice(me, from.uid, "No such ticket. Syntax: VIEW <id>");
return; return;
}; };
let state = if !t.open { "closed" } else if t.handler.is_some() { "taken" } else { "open" }; let state = if !t.open { t!(ctx, "closed") } else if t.handler.is_some() { t!(ctx, "taken") } else { t!(ctx, "open") };
ctx.notice(me, from.uid, format!("Ticket \x02#{}\x02 ({state}):", t.id)); ctx.notice(me, from.uid, t!(ctx, "Ticket \x02#{id}\x02 ({state}):", id = t.id, state = state));
ctx.notice(me, from.uid, format!(" From : \x02{}\x02", t.requester)); ctx.notice(me, from.uid, t!(ctx, " From : \x02{requester}\x02", requester = t.requester));
if let Some(h) = &t.handler { if let Some(h) = &t.handler {
ctx.notice(me, from.uid, format!(" Handler : \x02{h}\x02")); ctx.notice(me, from.uid, t!(ctx, " Handler : \x02{handler}\x02", handler = h));
} }
ctx.notice(me, from.uid, format!(" Opened : {}", human_time(t.ts))); ctx.notice(me, from.uid, t!(ctx, " Opened : {when}", when = human_time(t.ts)));
ctx.notice(me, from.uid, format!(" Message : {}", t.message)); ctx.notice(me, from.uid, t!(ctx, " Message : {msg}", msg = t.message));
} }

View file

@ -1,4 +1,4 @@
use echo_api::{NetView, Sender, ServiceCtx, Store}; use echo_api::{t, NetView, Sender, ServiceCtx, Store};
// ACTIVATE <account> / REJECT <account>: approve a pending vhost request (setting // ACTIVATE <account> / REJECT <account>: approve a pending vhost request (setting
// and applying it) or turn it down. Operators only. // and applying it) or turn it down. Operators only.
@ -14,30 +14,30 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
let host = match db.take_vhost_request(account) { let host = match db.take_vhost_request(account) {
Ok(Some(h)) => h, Ok(Some(h)) => h,
Ok(None) => { Ok(None) => {
ctx.notice(me, from.uid, format!("\x02{account}\x02 has no pending vhost request.")); ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 has no pending vhost request.", account = account));
return; return;
} }
Err(_) => { Err(_) => {
ctx.notice(me, from.uid, format!("\x02{account}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 isn't registered.", account = account));
return; return;
} }
}; };
if !activate { if !activate {
ctx.notice(me, from.uid, format!("Rejected \x02{account}\x02's vhost request.")); ctx.notice(me, from.uid, t!(ctx, "Rejected \x02{account}\x02's vhost request.", account = account));
return; return;
} }
// Re-check the requested host now (another account may have taken it since). // Re-check the requested host now (another account may have taken it since).
let host = match super::prepare_vhost(&host, account, db) { let host = match super::prepare_vhost(&host, account, db, ctx) {
Ok(h) => h, Ok(h) => h,
Err(msg) => { Err(msg) => {
ctx.notice(me, from.uid, format!("Can't activate: {msg}")); ctx.notice(me, from.uid, t!(ctx, "Can't activate: {msg}", msg = msg));
return; return;
} }
}; };
// The forbidden list may have grown since the request was filed; a user's // The forbidden list may have grown since the request was filed; a user's
// request must still obey it (an operator's own SET is a deliberate override). // request must still obey it (an operator's own SET is a deliberate override).
if db.vhost_is_forbidden(&host) { if db.vhost_is_forbidden(&host) {
ctx.notice(me, from.uid, format!("Can't activate: \x02{host}\x02 is on the forbidden list.")); ctx.notice(me, from.uid, t!(ctx, "Can't activate: \x02{host}\x02 is on the forbidden list.", host = host));
return; return;
} }
match db.set_vhost(account, &host, from.nick, None) { match db.set_vhost(account, &host, from.nick, None) {
@ -45,7 +45,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
for uid in net.uids_logged_into(account) { for uid in net.uids_logged_into(account) {
ctx.apply_vhost(&uid, &host); ctx.apply_vhost(&uid, &host);
} }
ctx.notice(me, from.uid, format!("Activated vhost \x02{host}\x02 for \x02{account}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Activated vhost \x02{host}\x02 for \x02{account}\x02.", host = host, account = account));
} }
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."),
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// DEFAULT: give yourself the auto-vhost from the network template, with your // DEFAULT: give yourself the auto-vhost from the network template, with your
// account name substituted for $account. // account name substituted for $account.
@ -18,7 +18,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store)
return; return;
} }
let generated = template.replace("$account", &label); let generated = template.replace("$account", &label);
let host = match super::prepare_vhost(&generated, account, db) { let host = match super::prepare_vhost(&generated, account, db, ctx) {
Ok(h) if !db.vhost_is_forbidden(&h) => h, Ok(h) if !db.vhost_is_forbidden(&h) => h,
_ => { _ => {
ctx.notice(me, from.uid, "Sorry, a vhost couldn't be generated for your account."); ctx.notice(me, from.uid, "Sorry, a vhost couldn't be generated for your account.");
@ -28,7 +28,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store)
match db.set_vhost(account, &host, "template", None) { match db.set_vhost(account, &host, "template", None) {
Ok(()) => { Ok(()) => {
ctx.apply_vhost(from.uid, &host); ctx.apply_vhost(from.uid, &host);
ctx.notice(me, from.uid, format!("You now have the vhost \x02{host}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You now have the vhost \x02{host}\x02.", host = host));
} }
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."),
} }

View file

@ -1,4 +1,4 @@
use echo_api::{NetView, Sender, ServiceCtx, Store}; use echo_api::{t, NetView, Sender, ServiceCtx, Store};
// DEL <account>: remove an account's vhost, restoring the normal host on any // DEL <account>: remove an account's vhost, restoring the normal host on any
// online sessions. Operators only. // online sessions. Operators only.
@ -27,9 +27,9 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
} }
} }
} }
ctx.notice(me, from.uid, format!("Vhost for \x02{account}\x02 removed.")); ctx.notice(me, from.uid, t!(ctx, "Vhost for \x02{account}\x02 removed.", account = account));
} }
Ok(false) => ctx.notice(me, from.uid, format!("\x02{account}\x02 has no vhost.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 has no vhost.", account = account)),
Err(_) => ctx.notice(me, from.uid, format!("\x02{account}\x02 isn't registered.")), Err(_) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 isn't registered.", account = account)),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// FORBID <pattern>: block user-requested vhosts matching this regex (operators), // FORBID <pattern>: block user-requested vhosts matching this regex (operators),
// e.g. (?i)(oper|admin|staff|services) to stop impersonation. // e.g. (?i)(oper|admin|staff|services) to stop impersonation.
@ -12,9 +12,9 @@ pub fn add(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mu
} }
let pattern = args[1..].join(" "); let pattern = args[1..].join(" ");
match db.vhost_forbid_add(&pattern) { match db.vhost_forbid_add(&pattern) {
Ok(true) => ctx.notice(me, from.uid, format!("Forbidden vhost pattern added: {pattern}")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Forbidden vhost pattern added: {pattern}", pattern = pattern)),
Ok(false) => ctx.notice(me, from.uid, "That pattern is already forbidden."), Ok(false) => ctx.notice(me, from.uid, "That pattern is already forbidden."),
Err(_) => ctx.notice(me, from.uid, format!("\x02{pattern}\x02 isn't a valid regular expression.")), Err(_) => ctx.notice(me, from.uid, t!(ctx, "\x02{pattern}\x02 isn't a valid regular expression.", pattern = pattern)),
} }
} }
@ -28,9 +28,9 @@ pub fn list(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
ctx.notice(me, from.uid, "No vhost patterns are forbidden."); ctx.notice(me, from.uid, "No vhost patterns are forbidden.");
return; return;
} }
ctx.notice(me, from.uid, format!("Forbidden vhost patterns ({}):", forbidden.len())); ctx.notice(me, from.uid, t!(ctx, "Forbidden vhost patterns ({count}):", count = forbidden.len()));
for (i, p) in forbidden.iter().enumerate() { for (i, p) in forbidden.iter().enumerate() {
ctx.notice(me, from.uid, format!(" {}. {p}", i + 1)); ctx.notice(me, from.uid, t!(ctx, " {n}. {p}", n = i + 1, p = p));
} }
} }
@ -44,8 +44,8 @@ pub fn del(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mu
return; return;
}; };
match db.vhost_forbid_del(n) { match db.vhost_forbid_del(n) {
Ok(Some(pattern)) => ctx.notice(me, from.uid, format!("Removed forbidden pattern: {pattern}")), Ok(Some(pattern)) => ctx.notice(me, from.uid, t!(ctx, "Removed forbidden pattern: {pattern}", pattern = pattern)),
Ok(None) => ctx.notice(me, from.uid, format!("There's no forbidden pattern #\x02{n}\x02.")), Ok(None) => ctx.notice(me, from.uid, t!(ctx, "There's no forbidden pattern #\x02{n}\x02.", n = n)),
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."),
} }
} }

View file

@ -3,7 +3,7 @@
//! with SET/DEL and review with LIST. `lib.rs` holds the dispatcher; each //! with SET/DEL and review with LIST. `lib.rs` holds the dispatcher; each
//! command lives in its own file. //! command lives in its own file.
use echo_api::{HelpEntry, NetView, Priv, Sender, Service, ServiceCtx, Store}; use echo_api::{t, HelpEntry, NetView, Priv, Sender, Service, ServiceCtx, Store};
#[path = "on.rs"] #[path = "on.rs"]
mod on; mod on;
@ -96,7 +96,7 @@ impl Service for HostServ {
Some("TEMPLATE") => template::handle(me, from, args, ctx, db), Some("TEMPLATE") => template::handle(me, from, args, ctx, db),
Some("DEFAULT") => default::handle(me, from, ctx, db), Some("DEFAULT") => default::handle(me, from, ctx, db),
Some("HELP") | None => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()), Some("HELP") | None => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()),
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "I don't know the command \x02{other}\x02. Try \x02HELP\x02.", other = other)),
} }
} }
} }
@ -125,13 +125,13 @@ pub(crate) fn normalize_vhost(spec: &str) -> String {
// Normalise a requested vhost and check it's valid and not already another // Normalise a requested vhost and check it's valid and not already another
// account's. Returns the canonical spec to store, or a message to show. // account's. Returns the canonical spec to store, or a message to show.
pub(crate) fn prepare_vhost(spec: &str, account: &str, db: &dyn Store) -> Result<String, String> { pub(crate) fn prepare_vhost(spec: &str, account: &str, db: &dyn Store, ctx: &ServiceCtx) -> Result<String, String> {
let host = normalize_vhost(spec); let host = normalize_vhost(spec);
if !valid_vhost(&host) { if !valid_vhost(&host) {
return Err(format!("\x02{host}\x02 isn't a valid host (letters, digits, hyphens and dots).")); return Err(t!(ctx, "\x02{host}\x02 isn't a valid host (letters, digits, hyphens and dots).", host = host));
} }
if db.vhost_owner(&host).is_some_and(|owner| !owner.eq_ignore_ascii_case(account)) { if db.vhost_owner(&host).is_some_and(|owner| !owner.eq_ignore_ascii_case(account)) {
return Err(format!("\x02{host}\x02 is already in use. Please choose another.")); return Err(t!(ctx, "\x02{host}\x02 is already in use. Please choose another.", host = host));
} }
Ok(host) Ok(host)
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// LIST: every account with an assigned vhost. Operators only. // LIST: every account with an assigned vhost. Operators only.
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
@ -10,9 +10,9 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
ctx.notice(me, from.uid, "No vhosts have been assigned."); ctx.notice(me, from.uid, "No vhosts have been assigned.");
return; return;
} }
ctx.notice(me, from.uid, format!("Assigned vhosts ({}):", vhosts.len())); ctx.notice(me, from.uid, t!(ctx, "Assigned vhosts ({count}):", count = vhosts.len()));
for v in &vhosts { for v in &vhosts {
let temp = if v.expires.is_some() { ", temporary" } else { "" }; let temp = if v.expires.is_some() { t!(ctx, ", temporary") } else { String::new() };
ctx.notice(me, from.uid, format!(" \x02{}\x02{} (by {}{temp})", v.account, v.host, v.setter)); ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02 — {host} (by {setter}{temp})", account = v.account, host = v.host, setter = v.setter, temp = temp));
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// OFFER <host>: add a vhost to the self-serve menu (operators). // OFFER <host>: add a vhost to the self-serve menu (operators).
pub fn add(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn add(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -10,11 +10,11 @@ pub fn add(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mu
return; return;
}; };
if !super::valid_vhost(host) { if !super::valid_vhost(host) {
ctx.notice(me, from.uid, format!("\x02{host}\x02 isn't a valid host.")); ctx.notice(me, from.uid, t!(ctx, "\x02{host}\x02 isn't a valid host.", host = host));
return; return;
} }
match db.vhost_offer_add(host) { match db.vhost_offer_add(host) {
Ok(true) => ctx.notice(me, from.uid, format!("\x02{host}\x02 is now on the vhost menu.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "\x02{host}\x02 is now on the vhost menu.", host = host)),
Ok(false) => ctx.notice(me, from.uid, "That vhost is already on the menu."), Ok(false) => ctx.notice(me, from.uid, "That vhost is already on the menu."),
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."),
} }
@ -27,9 +27,9 @@ pub fn list(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
ctx.notice(me, from.uid, "No vhosts are on offer."); ctx.notice(me, from.uid, "No vhosts are on offer.");
return; return;
} }
ctx.notice(me, from.uid, format!("Vhosts on offer ({}):", offers.len())); ctx.notice(me, from.uid, t!(ctx, "Vhosts on offer ({count}):", count = offers.len()));
for (i, host) in offers.iter().enumerate() { for (i, host) in offers.iter().enumerate() {
ctx.notice(me, from.uid, format!(" {}. {host}", i + 1)); ctx.notice(me, from.uid, t!(ctx, " {n}. {host}", n = i + 1, host = host));
} }
ctx.notice(me, from.uid, "Take one with \x02TAKE\x02 <number>."); ctx.notice(me, from.uid, "Take one with \x02TAKE\x02 <number>.");
} }
@ -44,8 +44,8 @@ pub fn del(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mu
return; return;
}; };
match db.vhost_offer_del(n) { match db.vhost_offer_del(n) {
Ok(Some(host)) => ctx.notice(me, from.uid, format!("Removed \x02{host}\x02 from the menu.")), Ok(Some(host)) => ctx.notice(me, from.uid, t!(ctx, "Removed \x02{host}\x02 from the menu.", host = host)),
Ok(None) => ctx.notice(me, from.uid, format!("There's no offer #\x02{n}\x02.")), Ok(None) => ctx.notice(me, from.uid, t!(ctx, "There's no offer #\x02{n}\x02.", n = n)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// ON: activate the vhost assigned to your account. // ON: activate the vhost assigned to your account.
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
@ -9,7 +9,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
match db.vhost(account) { match db.vhost(account) {
Some(v) => { Some(v) => {
ctx.apply_vhost(from.uid, &v.host); ctx.apply_vhost(from.uid, &v.host);
ctx.notice(me, from.uid, format!("Your vhost \x02{}\x02 is now active.", v.host)); ctx.notice(me, from.uid, t!(ctx, "Your vhost \x02{host}\x02 is now active.", host = v.host));
} }
None => ctx.notice(me, from.uid, "You have no vhost assigned."), None => ctx.notice(me, from.uid, "You have no vhost assigned."),
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// REQUEST <host>: ask for a vhost, to be approved by an operator. // REQUEST <host>: ask for a vhost, to be approved by an operator.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -10,7 +10,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
ctx.notice(me, from.uid, "Syntax: REQUEST <host>"); ctx.notice(me, from.uid, "Syntax: REQUEST <host>");
return; return;
}; };
let host = match super::prepare_vhost(host, account, db) { let host = match super::prepare_vhost(host, account, db, ctx) {
Ok(h) => h, Ok(h) => h,
Err(msg) => { Err(msg) => {
ctx.notice(me, from.uid, msg); ctx.notice(me, from.uid, msg);
@ -18,16 +18,16 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
}; };
if db.vhost_is_forbidden(&host) { if db.vhost_is_forbidden(&host) {
ctx.notice(me, from.uid, format!("\x02{host}\x02 isn't allowed here. Please choose another.")); ctx.notice(me, from.uid, t!(ctx, "\x02{host}\x02 isn't allowed here. Please choose another.", host = host));
return; return;
} }
let wait = db.vhost_request_wait(account); let wait = db.vhost_request_wait(account);
if wait > 0 { if wait > 0 {
ctx.notice(me, from.uid, format!("Please wait \x02{wait}\x02s before requesting another vhost.")); ctx.notice(me, from.uid, t!(ctx, "Please wait \x02{wait}\x02s before requesting another vhost.", wait = wait));
return; return;
} }
match db.request_vhost(account, &host) { match db.request_vhost(account, &host) {
Ok(()) => ctx.notice(me, from.uid, format!("Requested vhost \x02{host}\x02 — an operator will review it.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Requested vhost \x02{host}\x02 — an operator will review it.", host = host)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{parse_duration, NetView, Sender, ServiceCtx, Store}; use echo_api::{parse_duration, t, NetView, Sender, ServiceCtx, Store};
// SET <account> <host> [duration]: assign a vhost to an account, applying it at // SET <account> <host> [duration]: assign a vhost to an account, applying it at
// once to online sessions. An optional duration (e.g. 30d) makes it temporary. // once to online sessions. An optional duration (e.g. 30d) makes it temporary.
@ -12,10 +12,10 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
return; return;
}; };
if db.account(account).is_none() { if db.account(account).is_none() {
ctx.notice(me, from.uid, format!("\x02{account}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 isn't registered.", account = account));
return; return;
} }
let host = match super::prepare_vhost(host, account, db) { let host = match super::prepare_vhost(host, account, db, ctx) {
Ok(h) => h, Ok(h) => h,
Err(msg) => { Err(msg) => {
ctx.notice(me, from.uid, msg); ctx.notice(me, from.uid, msg);
@ -28,8 +28,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
for uid in net.uids_logged_into(account) { for uid in net.uids_logged_into(account) {
ctx.apply_vhost(&uid, &host); ctx.apply_vhost(&uid, &host);
} }
let when = args.get(3).filter(|_| ttl.is_some()).map(|d| format!(" (expires in {d})")).unwrap_or_default(); let when = args.get(3).filter(|_| ttl.is_some()).map(|d| t!(ctx, " (expires in {d})", d = d)).unwrap_or_default();
ctx.notice(me, from.uid, format!("Vhost \x02{host}\x02 assigned to \x02{account}\x02{when}.")); ctx.notice(me, from.uid, t!(ctx, "Vhost \x02{host}\x02 assigned to \x02{account}\x02{when}.", host = host, account = account, when = when));
} }
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."),
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// TAKE <number>: assign yourself the vhost offered at that position on the menu. // TAKE <number>: assign yourself the vhost offered at that position on the menu.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -11,10 +11,10 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
let Some(offer) = n.checked_sub(1).and_then(|i| db.vhost_offers().into_iter().nth(i)) else { let Some(offer) = n.checked_sub(1).and_then(|i| db.vhost_offers().into_iter().nth(i)) else {
ctx.notice(me, from.uid, format!("There's no offer #\x02{n}\x02. See \x02OFFERLIST\x02.")); ctx.notice(me, from.uid, t!(ctx, "There's no offer #\x02{n}\x02. See \x02OFFERLIST\x02.", n = n));
return; return;
}; };
let host = match super::prepare_vhost(&offer, account, db) { let host = match super::prepare_vhost(&offer, account, db, ctx) {
Ok(h) => h, Ok(h) => h,
Err(msg) => { Err(msg) => {
ctx.notice(me, from.uid, msg); ctx.notice(me, from.uid, msg);
@ -24,13 +24,13 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
// A menu offer can outlive a later FORBID; re-check at grant time like the // A menu offer can outlive a later FORBID; re-check at grant time like the
// request/approve/default paths, or a stale offer bypasses the impersonation guard. // request/approve/default paths, or a stale offer bypasses the impersonation guard.
if db.vhost_is_forbidden(&host) { if db.vhost_is_forbidden(&host) {
ctx.notice(me, from.uid, format!("\x02{host}\x02 isn't allowed here. Please choose another.")); ctx.notice(me, from.uid, t!(ctx, "\x02{host}\x02 isn't allowed here. Please choose another.", host = host));
return; return;
} }
match db.set_vhost(account, &host, "offer", None) { match db.set_vhost(account, &host, "offer", None) {
Ok(()) => { Ok(()) => {
ctx.apply_vhost(from.uid, &host); ctx.apply_vhost(from.uid, &host);
ctx.notice(me, from.uid, format!("You now have the vhost \x02{host}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You now have the vhost \x02{host}\x02.", host = host));
} }
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."),
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// TEMPLATE [<pattern>]: show the auto-vhost template, or (operators) set it. // TEMPLATE [<pattern>]: show the auto-vhost template, or (operators) set it.
// Use $account for the requester's sanitised account name, e.g. // Use $account for the requester's sanitised account name, e.g.
@ -6,7 +6,7 @@ use echo_api::{Sender, ServiceCtx, Store};
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
match args.get(1) { match args.get(1) {
None => match db.vhost_template() { None => match db.vhost_template() {
Some(t) => ctx.notice(me, from.uid, format!("Auto-vhost template: \x02{t}\x02. Users apply it with \x02DEFAULT\x02.")), Some(tmpl) => ctx.notice(me, from.uid, t!(ctx, "Auto-vhost template: \x02{tmpl}\x02. Users apply it with \x02DEFAULT\x02.", tmpl = tmpl)),
None => ctx.notice(me, from.uid, "No auto-vhost template is set."), None => ctx.notice(me, from.uid, "No auto-vhost template is set."),
}, },
Some(&arg) => { Some(&arg) => {
@ -24,7 +24,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
match db.set_vhost_template(Some(template.clone())) { match db.set_vhost_template(Some(template.clone())) {
Ok(()) => ctx.notice(me, from.uid, format!("Auto-vhost template set to \x02{template}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Auto-vhost template set to \x02{template}\x02.", template = template)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// WAITING: pending vhost requests awaiting approval. Operators only. // WAITING: pending vhost requests awaiting approval. Operators only.
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
@ -10,9 +10,9 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
ctx.notice(me, from.uid, "No vhost requests are waiting."); ctx.notice(me, from.uid, "No vhost requests are waiting.");
return; return;
} }
ctx.notice(me, from.uid, format!("Pending vhost requests ({}):", requests.len())); ctx.notice(me, from.uid, t!(ctx, "Pending vhost requests ({count}):", count = requests.len()));
for (account, host) in &requests { for (account, host) in &requests {
ctx.notice(me, from.uid, format!(" \x02{account}\x02{host}")); ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02 — {host}", account = account, host = host));
} }
ctx.notice(me, from.uid, "Approve with \x02ACTIVATE\x02 <account> or turn down with \x02REJECT\x02 <account>."); ctx.notice(me, from.uid, "Approve with \x02ACTIVATE\x02 <account> or turn down with \x02REJECT\x02 <account>.");
} }

View file

@ -1,4 +1,4 @@
use echo_api::{NewsKind, Priv, Sender, ServiceCtx, Store}; use echo_api::{t, NewsKind, Priv, Sender, ServiceCtx, Store};
// DEL/ODEL <number>: remove a bulletin by its listed position. Admin only. // DEL/ODEL <number>: remove a bulletin by its listed position. Admin only.
pub fn handle(me: &str, from: &Sender, kind: NewsKind, num: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, kind: NewsKind, num: Option<&str>, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -8,14 +8,14 @@ pub fn handle(me: &str, from: &Sender, kind: NewsKind, num: Option<&str>, ctx: &
} }
let which = if kind == super::OPER { "oper" } else { "public" }; let which = if kind == super::OPER { "oper" } else { "public" };
let Some(n) = num.and_then(|n| n.parse::<usize>().ok()) else { let Some(n) = num.and_then(|n| n.parse::<usize>().ok()) else {
ctx.notice(me, from.uid, format!("Syntax: {} <number>", if kind == super::OPER { "ODEL" } else { "DEL" })); ctx.notice(me, from.uid, t!(ctx, "Syntax: {cmd} <number>", cmd = if kind == super::OPER { "ODEL" } else { "DEL" }));
return; return;
}; };
match db.news(kind).get(n.wrapping_sub(1)) { match db.news(kind).get(n.wrapping_sub(1)) {
Some(item) if n >= 1 => { Some(item) if n >= 1 => {
db.news_del(item.id); db.news_del(item.id);
ctx.notice(me, from.uid, format!("Removed \x02{which}\x02 bulletin \x02{n}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Removed \x02{which}\x02 bulletin \x02{n}\x02.", which = which, n = n));
} }
_ => ctx.notice(me, from.uid, format!("There's no \x02{which}\x02 bulletin \x02{n}\x02.")), _ => ctx.notice(me, from.uid, t!(ctx, "There's no \x02{which}\x02 bulletin \x02{n}\x02.", which = which, n = n)),
} }
} }

View file

@ -9,7 +9,7 @@
//! and any operator may OLIST. `lib.rs` holds the dispatcher; each command //! and any operator may OLIST. `lib.rs` holds the dispatcher; each command
//! (parameterised by bulletin kind) lives in its own file. //! (parameterised by bulletin kind) lives in its own file.
use echo_api::{HelpEntry, NetView, NewsKind, Sender, Service, ServiceCtx, Store}; use echo_api::{t, HelpEntry, NetView, NewsKind, Sender, Service, ServiceCtx, Store};
#[path = "post.rs"] #[path = "post.rs"]
mod post; mod post;
@ -64,7 +64,7 @@ impl Service for InfoServ {
// Oper bulletins are for operators only. // Oper bulletins are for operators only.
Some("OLIST") => list::handle(me, from, OPER, true, ctx, db), Some("OLIST") => list::handle(me, from, OPER, true, 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()),
Some(other) => ctx.notice(me, from.uid, format!("I don't know \x02{other}\x02. Try \x02LIST\x02 or \x02HELP\x02.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "I don't know \x02{other}\x02. Try \x02LIST\x02 or \x02HELP\x02.", other = other)),
} }
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{NewsKind, Sender, ServiceCtx, Store}; use echo_api::{t, NewsKind, Sender, ServiceCtx, Store};
// LIST/OLIST: show the bulletins of a kind. Public is open; oper is oper-only. // LIST/OLIST: show the bulletins of a kind. Public is open; oper is oper-only.
pub fn handle(me: &str, from: &Sender, kind: NewsKind, oper_only: bool, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, kind: NewsKind, oper_only: bool, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -9,11 +9,11 @@ pub fn handle(me: &str, from: &Sender, kind: NewsKind, oper_only: bool, ctx: &mu
let items = db.news(kind); let items = db.news(kind);
let which = if kind == super::OPER { "oper" } else { "public" }; let which = if kind == super::OPER { "oper" } else { "public" };
if items.is_empty() { if items.is_empty() {
ctx.notice(me, from.uid, format!("There are no {which} bulletins.")); ctx.notice(me, from.uid, t!(ctx, "There are no {which} bulletins.", which = which));
return; return;
} }
for (i, item) in items.iter().enumerate() { for (i, item) in items.iter().enumerate() {
ctx.notice(me, from.uid, format!("{}. {} — by {}", i + 1, item.text, item.setter)); ctx.notice(me, from.uid, t!(ctx, "{num}. {text} — by {by}", num = i + 1, text = item.text, by = item.setter));
} }
ctx.notice(me, from.uid, format!("End of {which} bulletins ({} shown).", items.len())); ctx.notice(me, from.uid, t!(ctx, "End of {which} bulletins ({count} shown).", which = which, count = items.len()));
} }

View file

@ -1,4 +1,4 @@
use echo_api::{NewsKind, Priv, Sender, ServiceCtx, Store}; use echo_api::{t, NewsKind, Priv, Sender, ServiceCtx, Store};
// POST/OPOST <message>: add a bulletin (public or oper). Admin only. // POST/OPOST <message>: add a bulletin (public or oper). Admin only.
pub fn handle(me: &str, from: &Sender, kind: NewsKind, rest: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, kind: NewsKind, rest: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -8,11 +8,11 @@ pub fn handle(me: &str, from: &Sender, kind: NewsKind, rest: &[&str], ctx: &mut
} }
let message = rest.join(" "); let message = rest.join(" ");
if message.trim().is_empty() { if message.trim().is_empty() {
ctx.notice(me, from.uid, format!("Syntax: {} <message>", if kind == super::OPER { "OPOST" } else { "POST" })); ctx.notice(me, from.uid, t!(ctx, "Syntax: {cmd} <message>", cmd = if kind == super::OPER { "OPOST" } else { "POST" }));
return; return;
} }
let setter = from.account.unwrap_or(from.nick); let setter = from.account.unwrap_or(from.nick);
db.news_add(kind, &message, setter); db.news_add(kind, &message, setter);
let which = if kind == super::OPER { "oper" } else { "public" }; let which = if kind == super::OPER { "oper" } else { "public" };
ctx.notice(me, from.uid, format!("Added a \x02{which}\x02 bulletin.")); ctx.notice(me, from.uid, t!(ctx, "Added a \x02{which}\x02 bulletin.", which = which));
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// CANCEL <nick>: recall the last unread memo you sent to <nick>. A memo the // CANCEL <nick>: recall the last unread memo you sent to <nick>. A memo the
// recipient has already read can't be recalled. // recipient has already read can't be recalled.
@ -8,12 +8,12 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
return; return;
}; };
let Some(dest) = db.resolve_account(target).map(str::to_string) else { let Some(dest) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't registered.", target = target));
return; return;
}; };
if db.memo_cancel(&dest, account) { if db.memo_cancel(&dest, account) {
ctx.notice(me, from.uid, format!("Your last unread memo to \x02{target}\x02 has been cancelled.")); ctx.notice(me, from.uid, t!(ctx, "Your last unread memo to \x02{target}\x02 has been cancelled.", target = target));
} else { } else {
ctx.notice(me, from.uid, format!("You have no unread memo to \x02{target}\x02 to cancel.")); ctx.notice(me, from.uid, t!(ctx, "You have no unread memo to \x02{target}\x02 to cancel.", target = target));
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{human_time, Sender, ServiceCtx, Store}; use echo_api::{human_time, t, Sender, ServiceCtx, Store};
// CHECK <nick>: has the last memo you sent to <nick> been read yet? // CHECK <nick>: has the last memo you sent to <nick> been read yet?
pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -7,12 +7,12 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
return; return;
}; };
let Some(dest) = db.resolve_account(target).map(str::to_string) else { let Some(dest) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't registered.", target = target));
return; return;
}; };
match db.memo_check(&dest, account) { match db.memo_check(&dest, account) {
Some((true, ts)) => ctx.notice(me, from.uid, format!("Your last memo to \x02{target}\x02 (sent {}) has been \x02read\x02.", human_time(ts))), Some((true, ts)) => ctx.notice(me, from.uid, t!(ctx, "Your last memo to \x02{target}\x02 (sent {when}) has been \x02read\x02.", target = target, when = human_time(ts))),
Some((false, ts)) => ctx.notice(me, from.uid, format!("Your last memo to \x02{target}\x02 (sent {}) has \x02not\x02 been read yet.", human_time(ts))), Some((false, ts)) => ctx.notice(me, from.uid, t!(ctx, "Your last memo to \x02{target}\x02 (sent {when}) has \x02not\x02 been read yet.", target = target, when = human_time(ts))),
None => ctx.notice(me, from.uid, format!("You have no memo on record to \x02{target}\x02.")), None => ctx.notice(me, from.uid, t!(ctx, "You have no memo on record to \x02{target}\x02.", target = target)),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// DEL <num>|ALL: remove a memo (or the whole mailbox). // DEL <num>|ALL: remove a memo (or the whole mailbox).
pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -9,7 +9,7 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
for i in (0..count).rev() { for i in (0..count).rev() {
db.memo_del(account, i); db.memo_del(account, i);
} }
ctx.notice(me, from.uid, format!("Deleted all \x02{count}\x02 memo(s).")); ctx.notice(me, from.uid, t!(ctx, "Deleted all \x02{count}\x02 memo(s).", count = count));
} }
Some(numstr) => { Some(numstr) => {
let Some(n) = numstr.parse::<usize>().ok().filter(|n| *n >= 1) else { let Some(n) = numstr.parse::<usize>().ok().filter(|n| *n >= 1) else {
@ -17,9 +17,9 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
return; return;
}; };
if db.memo_del(account, n - 1) { if db.memo_del(account, n - 1) {
ctx.notice(me, from.uid, format!("Memo #\x02{n}\x02 deleted.")); ctx.notice(me, from.uid, t!(ctx, "Memo #\x02{n}\x02 deleted.", n = n));
} else { } else {
ctx.notice(me, from.uid, format!("You have no memo #\x02{n}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You have no memo #\x02{n}\x02.", n = n));
} }
} }
None => ctx.notice(me, from.uid, "Syntax: DEL <num>|ALL"), None => ctx.notice(me, from.uid, "Syntax: DEL <num>|ALL"),

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// IGNORE ADD <nick> | DEL <nick> | LIST: manage your memo-ignore list. Memos // IGNORE ADD <nick> | DEL <nick> | LIST: manage your memo-ignore list. Memos
// from an ignored account are silently dropped. // from an ignored account are silently dropped.
@ -14,13 +14,13 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
// bound — mirrors AJOIN/CERT; unthrottled for identified users otherwise. // bound — mirrors AJOIN/CERT; unthrottled for identified users otherwise.
const MAX_IGNORE: usize = 50; const MAX_IGNORE: usize = 50;
if db.memo_ignores(account).len() >= MAX_IGNORE { if db.memo_ignores(account).len() >= MAX_IGNORE {
ctx.notice(me, from.uid, format!("Your memo-ignore list is full (max {MAX_IGNORE}).")); ctx.notice(me, from.uid, t!(ctx, "Your memo-ignore list is full (max {max}).", max = MAX_IGNORE));
return; return;
} }
if db.memo_ignore_add(account, &target) { if db.memo_ignore_add(account, &target) {
ctx.notice(me, from.uid, format!("Now ignoring memos from \x02{target}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Now ignoring memos from \x02{target}\x02.", target = target));
} else { } else {
ctx.notice(me, from.uid, format!("You're already ignoring \x02{target}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You're already ignoring \x02{target}\x02.", target = target));
} }
} }
Some("DEL") | Some("REMOVE") => { Some("DEL") | Some("REMOVE") => {
@ -30,9 +30,9 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
}; };
let target = db.resolve_account(nick).map(str::to_string).unwrap_or_else(|| nick.to_string()); let target = db.resolve_account(nick).map(str::to_string).unwrap_or_else(|| nick.to_string());
if db.memo_ignore_del(account, &target) { if db.memo_ignore_del(account, &target) {
ctx.notice(me, from.uid, format!("No longer ignoring \x02{target}\x02.")); ctx.notice(me, from.uid, t!(ctx, "No longer ignoring \x02{target}\x02.", target = target));
} else { } else {
ctx.notice(me, from.uid, format!("You weren't ignoring \x02{target}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You weren't ignoring \x02{target}\x02.", target = target));
} }
} }
Some("LIST") | None => { Some("LIST") | None => {
@ -40,7 +40,7 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
if list.is_empty() { if list.is_empty() {
ctx.notice(me, from.uid, "Your memo-ignore list is empty."); ctx.notice(me, from.uid, "Your memo-ignore list is empty.");
} else { } else {
ctx.notice(me, from.uid, format!("You're ignoring memos from: {}", list.join(", "))); ctx.notice(me, from.uid, t!(ctx, "You're ignoring memos from: {names}", names = list.join(", ")));
} }
} }
_ => ctx.notice(me, from.uid, "Syntax: IGNORE ADD <nick> | DEL <nick> | LIST"), _ => ctx.notice(me, from.uid, "Syntax: IGNORE ADD <nick> | DEL <nick> | LIST"),

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// INFO: a summary of your mailbox (total, unread, capacity). // INFO: a summary of your mailbox (total, unread, capacity).
pub fn handle(me: &str, from: &Sender, account: &str, ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, account: &str, ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -7,6 +7,6 @@ pub fn handle(me: &str, from: &Sender, account: &str, ctx: &mut ServiceCtx, db:
ctx.notice( ctx.notice(
me, me,
from.uid, from.uid,
format!("You have \x02{total}\x02 memo(s), \x02{unread}\x02 unread, of a maximum \x02{}\x02.", super::MAX_MEMOS), t!(ctx, "You have \x02{total}\x02 memo(s), \x02{unread}\x02 unread, of a maximum \x02{max}\x02.", total = total, unread = unread, max = super::MAX_MEMOS),
); );
} }

View file

@ -4,7 +4,7 @@
//! account data. `lib.rs` holds the dispatcher; each command lives in its own //! account data. `lib.rs` holds the dispatcher; each command lives in its own
//! file, matching NickServ/ChanServ. //! file, matching NickServ/ChanServ.
use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; use echo_api::{t, HelpEntry, NetView, Sender, Service, ServiceCtx, Store};
#[path = "send.rs"] #[path = "send.rs"]
mod send; mod send;
@ -93,7 +93,7 @@ impl Service for MemoServ {
Some("STAFF") => staff::handle(me, from, account, args, ctx, net, db), Some("STAFF") => staff::handle(me, from, account, args, ctx, net, db),
Some("IGNORE") => ignore::handle(me, from, account, args, ctx, db), Some("IGNORE") => ignore::handle(me, from, account, args, ctx, db),
Some("SET") => set::handle(me, from, account, args, ctx, db), Some("SET") => set::handle(me, from, account, args, ctx, db),
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), Some(other) => ctx.notice(me, from.uid, t!(ctx, "I don't know the command \x02{other}\x02. Try \x02HELP\x02.", other = other)),
None => {} None => {}
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{human_time, Sender, ServiceCtx, Store}; use echo_api::{human_time, t, Sender, ServiceCtx, Store};
// LIST: show every memo with a one-line preview; \x02*\x02 marks unread. // LIST: show every memo with a one-line preview; \x02*\x02 marks unread.
pub fn handle(me: &str, from: &Sender, account: &str, ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, account: &str, ctx: &mut ServiceCtx, db: &dyn Store) {
@ -8,10 +8,10 @@ pub fn handle(me: &str, from: &Sender, account: &str, ctx: &mut ServiceCtx, db:
return; return;
} }
let unread = memos.iter().filter(|m| !m.read).count(); let unread = memos.iter().filter(|m| !m.read).count();
ctx.notice(me, from.uid, format!("Your memos ({} total, {unread} new). \x02*\x02 marks unread:", memos.len())); ctx.notice(me, from.uid, t!(ctx, "Your memos ({total} total, {unread} new). \x02*\x02 marks unread:", total = memos.len(), unread = unread));
for (i, m) in memos.iter().enumerate() { for (i, m) in memos.iter().enumerate() {
let flag = if m.read { ' ' } else { '*' }; let flag = if m.read { ' ' } else { '*' };
ctx.notice(me, from.uid, format!(" {}{flag} from \x02{}\x02 ({}): {}", i + 1, m.from, human_time(m.ts), preview(&m.text))); ctx.notice(me, from.uid, t!(ctx, " {num}{flag} from \x02{from}\x02 ({when}): {preview}", num = i + 1, flag = flag, from = m.from, when = human_time(m.ts), preview = preview(&m.text)));
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{human_time, MemoView, Sender, ServiceCtx, Store}; use echo_api::{human_time, t, MemoView, Sender, ServiceCtx, Store};
// READ <num>|NEW|ALL: display memos and mark them read. // READ <num>|NEW|ALL: display memos and mark them read.
pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -33,7 +33,7 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
show(me, from, n - 1, &m, ctx); show(me, from, n - 1, &m, ctx);
send_receipt(account, &m, db); send_receipt(account, &m, db);
} }
None => ctx.notice(me, from.uid, format!("You have no memo #\x02{n}\x02.")), None => ctx.notice(me, from.uid, t!(ctx, "You have no memo #\x02{n}\x02.", n = n)),
} }
} }
None => ctx.notice(me, from.uid, "Syntax: READ <num>|NEW|ALL"), None => ctx.notice(me, from.uid, "Syntax: READ <num>|NEW|ALL"),
@ -41,8 +41,8 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
} }
fn show(me: &str, from: &Sender, index: usize, m: &MemoView, ctx: &mut ServiceCtx) { fn show(me: &str, from: &Sender, index: usize, m: &MemoView, ctx: &mut ServiceCtx) {
ctx.notice(me, from.uid, format!("Memo #\x02{}\x02 from \x02{}\x02 ({}):", index + 1, m.from, human_time(m.ts))); ctx.notice(me, from.uid, t!(ctx, "Memo #\x02{num}\x02 from \x02{from}\x02 ({when}):", num = index + 1, from = m.from, when = human_time(m.ts)));
ctx.notice(me, from.uid, format!(" {}", m.text)); ctx.notice(me, from.uid, t!(ctx, " {text}", text = m.text));
} }
// If this was an unread RSEND memo, memo the sender that it's now been read. // If this was an unread RSEND memo, memo the sender that it's now been read.

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
use super::MAX_MEMOS; use super::MAX_MEMOS;
@ -7,28 +7,28 @@ use super::MAX_MEMOS;
pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store, receipt: bool) { pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store, receipt: bool) {
let cmd = if receipt { "RSEND" } else { "SEND" }; let cmd = if receipt { "RSEND" } else { "SEND" };
if args.len() < 3 { if args.len() < 3 {
ctx.notice(me, from.uid, format!("Syntax: {cmd} <nick> <text>")); ctx.notice(me, from.uid, t!(ctx, "Syntax: {cmd} <nick> <text>", cmd = cmd));
return; return;
} }
let target = args[1]; let target = args[1];
let text = args[2..].join(" "); let text = args[2..].join(" ");
let Some(dest) = db.resolve_account(target).map(str::to_string) else { let Some(dest) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't registered.", target = target));
return; return;
}; };
let limit = db.memo_limit_of(&dest).unwrap_or(MAX_MEMOS as u32) as usize; let limit = db.memo_limit_of(&dest).unwrap_or(MAX_MEMOS as u32) as usize;
if db.memo_list(&dest).len() >= limit { if db.memo_list(&dest).len() >= limit {
ctx.notice(me, from.uid, format!("\x02{target}\x02's mailbox is full — they'll need to clear some memos first.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02's mailbox is full — they'll need to clear some memos first.", target = target));
return; return;
} }
// If the recipient is ignoring the sender, drop it silently — the sender is // If the recipient is ignoring the sender, drop it silently — the sender is
// told it was sent, so the ignore isn't revealed. // told it was sent, so the ignore isn't revealed.
if db.memo_is_ignored(&dest, account) { if db.memo_is_ignored(&dest, account) {
ctx.notice(me, from.uid, format!("Memo sent to \x02{target}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Memo sent to \x02{target}\x02.", target = target));
return; return;
} }
match db.memo_send(&dest, account, &text, receipt) { match db.memo_send(&dest, account, &text, receipt) {
Ok(()) => ctx.notice(me, from.uid, format!("Memo sent to \x02{target}\x02.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Memo sent to \x02{target}\x02.", target = target)),
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."),
} }
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Priv, Sender, ServiceCtx, Store}; use echo_api::{t, Priv, Sender, ServiceCtx, Store};
// SENDALL <text>: leave a memo on every registered account. Admin-only, for // SENDALL <text>: leave a memo on every registered account. Admin-only, for
// network-wide announcements that persist until read. // network-wide announcements that persist until read.
@ -20,5 +20,5 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
sent += 1; sent += 1;
} }
} }
ctx.notice(me, from.uid, format!("Memo sent to \x02{sent}\x02 account(s).")); ctx.notice(me, from.uid, t!(ctx, "Memo sent to \x02{sent}\x02 account(s).", sent = sent));
} }

View file

@ -1,4 +1,4 @@
use echo_api::{Sender, ServiceCtx, Store}; use echo_api::{t, Sender, ServiceCtx, Store};
// SET NOTIFY {ON|OFF} | SET LIMIT <n>|NONE: your MemoServ preferences. // SET NOTIFY {ON|OFF} | SET LIMIT <n>|NONE: your MemoServ preferences.
pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -13,7 +13,7 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
} }
}; };
db.set_memo_notify(account, on); db.set_memo_notify(account, on);
ctx.notice(me, from.uid, format!("New-memo notification is now \x02{}\x02.", if on { "on" } else { "off" })); ctx.notice(me, from.uid, t!(ctx, "New-memo notification is now \x02{state}\x02.", state = if on { "on" } else { "off" }));
} }
Some("LIMIT") => { Some("LIMIT") => {
let limit = match args.get(2) { let limit = match args.get(2) {
@ -22,15 +22,15 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
Some(&n) => match n.parse::<u32>() { Some(&n) => match n.parse::<u32>() {
Ok(v) if v <= super::MAX_MEMOS as u32 => Some(v), Ok(v) if v <= super::MAX_MEMOS as u32 => Some(v),
_ => { _ => {
ctx.notice(me, from.uid, format!("The limit must be a number from 0 to {}, or NONE.", super::MAX_MEMOS)); ctx.notice(me, from.uid, t!(ctx, "The limit must be a number from 0 to {max}, or NONE.", max = super::MAX_MEMOS));
return; return;
} }
}, },
}; };
db.set_memo_limit(account, limit); db.set_memo_limit(account, limit);
match limit { match limit {
Some(v) => ctx.notice(me, from.uid, format!("Your mailbox limit is now \x02{v}\x02.")), Some(v) => ctx.notice(me, from.uid, t!(ctx, "Your mailbox limit is now \x02{v}\x02.", v = v)),
None => ctx.notice(me, from.uid, format!("Your mailbox limit is now the network default (\x02{}\x02).", super::MAX_MEMOS)), None => ctx.notice(me, from.uid, t!(ctx, "Your mailbox limit is now the network default (\x02{max}\x02).", max = super::MAX_MEMOS)),
} }
} }
_ => ctx.notice(me, from.uid, "Syntax: SET NOTIFY {ON|OFF} | SET LIMIT <n>|NONE"), _ => ctx.notice(me, from.uid, "Syntax: SET NOTIFY {ON|OFF} | SET LIMIT <n>|NONE"),

View file

@ -1,4 +1,4 @@
use echo_api::{NetView, Priv, Sender, ServiceCtx, Store}; use echo_api::{t, NetView, Priv, Sender, ServiceCtx, Store};
// STAFF <text>: leave a memo on every operator's account. Admin-only, for // STAFF <text>: leave a memo on every operator's account. Admin-only, for
// notes to the staff team that persist until read. Recipients are the union of // notes to the staff team that persist until read. Recipients are the union of
@ -29,5 +29,5 @@ pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut S
sent += 1; sent += 1;
} }
} }
ctx.notice(me, from.uid, format!("Memo sent to \x02{sent}\x02 operator(s).")); ctx.notice(me, from.uid, t!(ctx, "Memo sent to \x02{sent}\x02 operator(s).", sent = sent));
} }

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
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.
const MAX_AJOIN: usize = 25; const MAX_AJOIN: usize = 25;
@ -23,12 +24,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
let key = args.get(3).copied().unwrap_or(""); let key = args.get(3).copied().unwrap_or("");
if db.ajoin_list(account).len() >= MAX_AJOIN { if db.ajoin_list(account).len() >= MAX_AJOIN {
ctx.notice(me, from.uid, format!("Your auto-join list is full (max {MAX_AJOIN}).")); ctx.notice(me, from.uid, t!(ctx, "Your auto-join list is full (max {max}).", max = MAX_AJOIN));
return; return;
} }
match db.ajoin_add(account, channel, key) { match db.ajoin_add(account, channel, key) {
Ok(true) => ctx.notice(me, from.uid, format!("Added \x02{channel}\x02 to your auto-join list.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Added \x02{channel}\x02 to your auto-join list.", channel = channel)),
Ok(false) => ctx.notice(me, from.uid, format!("Updated the key for \x02{channel}\x02 on your auto-join list.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "Updated the key for \x02{channel}\x02 on your auto-join list.", channel = channel)),
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."),
} }
} }
@ -38,8 +39,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
match db.ajoin_del(account, channel) { match db.ajoin_del(account, channel) {
Ok(true) => ctx.notice(me, from.uid, format!("Removed \x02{channel}\x02 from your auto-join list.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Removed \x02{channel}\x02 from your auto-join list.", channel = channel)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{channel}\x02 isn't on your auto-join list.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{channel}\x02 isn't on your auto-join list.", channel = channel)),
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."),
} }
} }
@ -49,15 +50,15 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
ctx.notice(me, from.uid, "Your auto-join list is empty. Add channels with \x02AJOIN ADD <#channel>\x02."); ctx.notice(me, from.uid, "Your auto-join list is empty. Add channels with \x02AJOIN ADD <#channel>\x02.");
return; return;
} }
ctx.notice(me, from.uid, format!("Your auto-join list ({}):", list.len())); ctx.notice(me, from.uid, t!(ctx, "Your auto-join list ({count}):", count = list.len()));
for e in &list { for e in &list {
if e.key.is_empty() { if e.key.is_empty() {
ctx.notice(me, from.uid, format!(" \x02{}\x02", e.channel)); ctx.notice(me, from.uid, t!(ctx, " \x02{channel}\x02", channel = e.channel));
} else { } else {
ctx.notice(me, from.uid, format!(" \x02{}\x02 (key: {})", e.channel, e.key)); ctx.notice(me, from.uid, t!(ctx, " \x02{channel}\x02 (key: {key})", channel = e.channel, key = e.key));
} }
} }
} }
Some(other) => ctx.notice(me, from.uid, format!("Unknown AJOIN command \x02{other}\x02. Use \x02ADD\x02, \x02DEL\x02 or \x02LIST\x02.")), 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)),
} }
} }

View file

@ -1,4 +1,5 @@
use echo_api::{access_role, Sender, ServiceCtx, Store}; use echo_api::{access_role, Sender, ServiceCtx, Store};
use echo_api::t;
// ALIST: list the channels the sender's account founds or has access on. // ALIST: list the channels the sender's account founds or has access on.
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
@ -21,8 +22,8 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
return; return;
} }
rows.sort_by(|a, b| a.0.cmp(&b.0)); rows.sort_by(|a, b| a.0.cmp(&b.0));
ctx.notice(me, from.uid, format!("Channels you have access on ({}):", rows.len())); ctx.notice(me, from.uid, t!(ctx, "Channels you have access on ({count}):", count = rows.len()));
for (chan, role) in rows { for (chan, role) in rows {
ctx.notice(me, from.uid, format!(" \x02{chan}\x02 ({role})")); ctx.notice(me, from.uid, t!(ctx, " \x02{chan}\x02 ({role})", chan = chan, role = role));
} }
} }

View file

@ -1,5 +1,6 @@
use echo_api::{CertError, Store}; use echo_api::{CertError, Store};
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// CERT ADD|DEL|LIST [password] [fingerprint]: manage the TLS certificate // CERT ADD|DEL|LIST [password] [fingerprint]: manage the TLS certificate
// fingerprints that may log in to your account via SASL EXTERNAL. An identified // fingerprints that may log in to your account via SASL EXTERNAL. An identified
@ -16,7 +17,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if fps.is_empty() { if fps.is_empty() {
ctx.notice(me, from.uid, "No certificate fingerprints are registered to your account."); ctx.notice(me, from.uid, "No certificate fingerprints are registered to your account.");
} else { } else {
ctx.notice(me, from.uid, format!("Registered fingerprints: {}", fps.join(", "))); ctx.notice(me, from.uid, t!(ctx, "Registered fingerprints: {list}", list = fps.join(", ")));
} }
} }
Some("ADD") => { Some("ADD") => {
@ -28,7 +29,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
match db.certfp_add(&account, fp) { match db.certfp_add(&account, fp) {
Ok(()) => ctx.notice(me, from.uid, format!("Added fingerprint \x02{fp}\x02. You can now log in with SASL EXTERNAL.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Added fingerprint \x02{fp}\x02. You can now log in with SASL EXTERNAL.", fp = fp)),
Err(CertError::InUse) => ctx.notice(me, from.uid, "That fingerprint is already registered."), Err(CertError::InUse) => ctx.notice(me, from.uid, "That fingerprint is already registered."),
Err(CertError::Invalid) => ctx.notice(me, from.uid, "That does not look like a certificate fingerprint."), Err(CertError::Invalid) => ctx.notice(me, from.uid, "That does not look like a certificate fingerprint."),
Err(CertError::Full) => ctx.notice(me, from.uid, "You have too many fingerprints registered. Remove one first."), Err(CertError::Full) => ctx.notice(me, from.uid, "You have too many fingerprints registered. Remove one first."),
@ -44,7 +45,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
match db.certfp_del(&account, fp) { match db.certfp_del(&account, fp) {
Ok(true) => ctx.notice(me, from.uid, format!("Removed fingerprint \x02{fp}\x02.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Removed fingerprint \x02{fp}\x02.", fp = fp)),
Ok(false) => ctx.notice(me, from.uid, "No such fingerprint is registered to your account."), Ok(false) => ctx.notice(me, from.uid, "No such fingerprint is registered to your account."),
Err(_) => ctx.notice(me, from.uid, "Could not remove the fingerprint, please try again later."), Err(_) => ctx.notice(me, from.uid, "Could not remove the fingerprint, please try again later."),
} }
@ -75,7 +76,7 @@ fn resolve<'a>(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store
// verify can't be spammed to stall the engine. // verify can't be spammed to stall the engine.
fn verify(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store, password: &str) -> Option<String> { fn verify(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &mut dyn Store, password: &str) -> Option<String> {
if let Some(secs) = db.auth_lockout(from.nick) { if let Some(secs) = db.auth_lockout(from.nick) {
ctx.notice(me, from.uid, format!("Too many failed attempts. Please wait {secs}s and try again.")); ctx.notice(me, from.uid, t!(ctx, "Too many failed attempts. Please wait {secs}s and try again.", secs = secs));
return None; return None;
} }
match db.authenticate(from.nick, password).map(str::to_string) { match db.authenticate(from.nick, password).map(str::to_string) {

View file

@ -1,5 +1,6 @@
use echo_api::{CodeKind, Store}; use echo_api::{CodeKind, Store};
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// CONFIRM <code>: confirm your account's email with the code you were emailed. // CONFIRM <code>: confirm your account's email with the code you were emailed.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -12,7 +13,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
if db.is_verified(&account) { if db.is_verified(&account) {
ctx.notice(me, from.uid, format!("\x02{account}\x02 is already confirmed.")); ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 is already confirmed.", account = account));
return; return;
} }
if !db.take_code(&account, CodeKind::Confirm, code) { if !db.take_code(&account, CodeKind::Confirm, code) {
@ -20,7 +21,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
} }
match db.verify_account(&account) { match db.verify_account(&account) {
Ok(()) => ctx.notice(me, from.uid, format!("\x02{account}\x02 is now confirmed. Thanks!")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 is now confirmed. Thanks!", account = account)),
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."),
} }
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// DROP <password>: delete your account. Re-authenticates as confirmation, releases // DROP <password>: delete your account. Re-authenticates as confirmation, releases
// and drops the channels you found, and logs you out. // and drops the channels you found, and logs you out.
@ -16,7 +17,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
// Throttle the confirmation verify like IDENTIFY so the ~1s check can't be // Throttle the confirmation verify like IDENTIFY so the ~1s check can't be
// spammed to stall the engine, and record the attempt. // spammed to stall the engine, and record the attempt.
if let Some(secs) = db.auth_lockout(account) { if let Some(secs) = db.auth_lockout(account) {
ctx.notice(me, from.uid, format!("Too many failed attempts. Please wait {secs}s and try again.")); ctx.notice(me, from.uid, t!(ctx, "Too many failed attempts. Please wait {secs}s and try again.", secs = secs));
return; return;
} }
if db.authenticate(account, password).is_none() { if db.authenticate(account, password).is_none() {
@ -34,12 +35,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
for uid in net.uids_logged_into(account) { for uid in net.uids_logged_into(account) {
ctx.logout(&uid); ctx.logout(&uid);
} }
ctx.notice(me, from.uid, format!("Your account \x02{account}\x02 has been dropped.")); ctx.notice(me, from.uid, t!(ctx, "Your account \x02{account}\x02 has been dropped.", account = account));
if !dropped.is_empty() { if !dropped.is_empty() {
ctx.notice(me, from.uid, format!("Channels released: {}.", dropped.join(", "))); ctx.notice(me, from.uid, t!(ctx, "Channels released: {list}.", list = dropped.join(", ")));
} }
if !inherited.is_empty() { if !inherited.is_empty() {
let list: Vec<String> = inherited.iter().map(|(c, s)| format!("{c} -> {s}")).collect(); let list: Vec<String> = inherited.iter().map(|(c, s)| format!("{c} -> {s}")).collect();
ctx.notice(me, from.uid, format!("Channels passed to their successor: {}.", list.join(", "))); ctx.notice(me, from.uid, t!(ctx, "Channels passed to their successor: {list}.", list = list.join(", ")));
} }
} }

View file

@ -1,4 +1,5 @@
use echo_api::{Priv, Sender, ServiceCtx, Store}; use echo_api::{Priv, Sender, ServiceCtx, Store};
use echo_api::t;
// GETEMAIL <email>: list accounts registered with a matching email. Oper-only. // GETEMAIL <email>: list accounts registered with a matching email. Oper-only.
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &dyn Store) {
@ -12,8 +13,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
let accounts = db.accounts_by_email(pattern); let accounts = db.accounts_by_email(pattern);
if accounts.is_empty() { if accounts.is_empty() {
ctx.notice(me, from.uid, format!("No accounts use an email matching \x02{pattern}\x02.")); ctx.notice(me, from.uid, t!(ctx, "No accounts use an email matching \x02{pattern}\x02.", pattern = pattern));
return; return;
} }
ctx.notice(me, from.uid, format!("Accounts with email matching \x02{pattern}\x02: {}", accounts.join(", "))); ctx.notice(me, from.uid, t!(ctx, "Accounts with email matching \x02{pattern}\x02: {list}", pattern = pattern, list = accounts.join(", ")));
} }

View file

@ -1,6 +1,7 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::NetView; use echo_api::NetView;
use echo_api::t;
// GHOST/RECOVER <nick> [password]: rename off a session using a nick you own, // GHOST/RECOVER <nick> [password]: rename off a session using a nick you own,
// either by being identified to its account or giving that account's password. // either by being identified to its account or giving that account's password.
@ -11,11 +12,11 @@ use echo_api::NetView;
pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store, regain: bool) { pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store, regain: bool) {
let cmd = if regain { "RECOVER" } else { "GHOST" }; let cmd = if regain { "RECOVER" } else { "GHOST" };
let Some(&target) = args.get(1) else { let Some(&target) = args.get(1) else {
ctx.notice(me, from.uid, format!("Syntax: {cmd} <nick> [password]")); ctx.notice(me, from.uid, t!(ctx, "Syntax: {cmd} <nick> [password]", cmd = cmd));
return; return;
}; };
let Some(account) = db.resolve_account(target).map(str::to_string) else { let Some(account) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't registered.", target = target));
return; return;
}; };
// Ownership by an identified session, or by password — throttled and recorded // Ownership by an identified session, or by password — throttled and recorded
@ -25,7 +26,7 @@ pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ar
true true
} else if let Some(&pw) = args.get(2) { } else if let Some(&pw) = args.get(2) {
if let Some(secs) = db.auth_lockout(&account) { if let Some(secs) = db.auth_lockout(&account) {
ctx.notice(me, from.uid, format!("Too many failed attempts. Please wait {secs}s and try again.")); ctx.notice(me, from.uid, t!(ctx, "Too many failed attempts. Please wait {secs}s and try again.", secs = secs));
return; return;
} }
let ok = db.authenticate(&account, pw).is_some(); let ok = db.authenticate(&account, pw).is_some();
@ -38,7 +39,7 @@ pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ar
false false
}; };
if !owns { if !owns {
ctx.notice(me, from.uid, format!("Access denied. Identify to \x02{account}\x02 or give its password.")); ctx.notice(me, from.uid, t!(ctx, "Access denied. Identify to \x02{account}\x02 or give its password.", account = account));
return; return;
} }
// Free the nick if someone else is holding it. // Free the nick if someone else is holding it.
@ -50,10 +51,10 @@ pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ar
Some(ghost) => { Some(ghost) => {
let guest = echo_api::next_guest_nick(guest_nick, guest_seq, net, &*db); let guest = echo_api::next_guest_nick(guest_nick, guest_seq, net, &*db);
ctx.force_nick(&ghost, &guest); ctx.force_nick(&ghost, &guest);
ctx.notice(me, from.uid, format!("\x02{target}\x02 has been freed.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 has been freed.", target = target));
} }
None if !regain => { None if !regain => {
ctx.notice(me, from.uid, format!("Nobody is using \x02{target}\x02.")); ctx.notice(me, from.uid, t!(ctx, "Nobody is using \x02{target}\x02.", target = target));
return; return;
} }
None => {} // already free — RECOVER will simply regain it None => {} // already free — RECOVER will simply regain it
@ -61,6 +62,6 @@ pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ar
// RECOVER puts the caller back onto the nick (queued after any guest-rename). // RECOVER puts the caller back onto the nick (queued after any guest-rename).
if regain { if regain {
ctx.force_nick(from.uid, target); ctx.force_nick(from.uid, target);
ctx.notice(me, from.uid, format!("You have regained \x02{target}\x02.")); ctx.notice(me, from.uid, t!(ctx, "You have regained \x02{target}\x02.", target = target));
} }
} }

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// GLIST: list the nicks grouped to your account. // GLIST: list the nicks grouped to your account.
pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
@ -9,9 +10,9 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
}; };
let mut nicks = db.grouped_nicks(account); let mut nicks = db.grouped_nicks(account);
nicks.sort(); nicks.sort();
ctx.notice(me, from.uid, format!("Nicks grouped to \x02{account}\x02:")); ctx.notice(me, from.uid, t!(ctx, "Nicks grouped to \x02{account}\x02:", account = account));
ctx.notice(me, from.uid, format!(" \x02{account}\x02 (main)")); ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02 (main)", account = account));
for n in nicks { for n in nicks {
ctx.notice(me, from.uid, format!(" \x02{n}\x02")); ctx.notice(me, from.uid, t!(ctx, " \x02{n}\x02", n = n));
} }
} }

View file

@ -1,5 +1,6 @@
use echo_api::Store; use echo_api::Store;
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// GROUP <account> <password>: link your current nick to an existing account, so // GROUP <account> <password>: link your current nick to an existing account, so
// you can identify to it under this nick too. // you can identify to it under this nick too.
@ -12,7 +13,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
// password-guessing oracle against any account (and each ~1s verify blocks the // password-guessing oracle against any account (and each ~1s verify blocks the
// engine). Also feeds the auth audit feed. // engine). Also feeds the auth audit feed.
if let Some(secs) = db.auth_lockout(account) { if let Some(secs) = db.auth_lockout(account) {
ctx.notice(me, from.uid, format!("Too many failed attempts. Please wait {secs}s and try again.")); ctx.notice(me, from.uid, t!(ctx, "Too many failed attempts. Please wait {secs}s and try again.", secs = secs));
return; return;
} }
let Some(canonical) = db.authenticate(account, password).map(str::to_string) else { let Some(canonical) = db.authenticate(account, password).map(str::to_string) else {
@ -23,7 +24,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
db.note_auth(account, true); db.note_auth(account, true);
if db.account(from.nick).is_some() { if db.account(from.nick).is_some() {
ctx.notice(me, from.uid, format!("\x02{}\x02 is itself a registered account.", from.nick)); ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 is itself a registered account.", nick = from.nick));
return; return;
} }
// Guard the namespace like REGISTER does: grouping RESERVES from.nick (SET KILL // Guard the namespace like REGISTER does: grouping RESERVES from.nick (SET KILL
@ -36,16 +37,16 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
} }
if db.is_forbidden(echo_api::ForbidKind::Nick, from.nick).is_some() { if db.is_forbidden(echo_api::ForbidKind::Nick, from.nick).is_some() {
ctx.notice(me, from.uid, format!("The nick \x02{}\x02 is reserved and can't be grouped.", from.nick)); ctx.notice(me, from.uid, t!(ctx, "The nick \x02{nick}\x02 is reserved and can't be grouped.", nick = from.nick));
return; return;
} }
const MAX_GROUPED: usize = 25; const MAX_GROUPED: usize = 25;
if db.grouped_nicks(&canonical).len() >= MAX_GROUPED { if db.grouped_nicks(&canonical).len() >= MAX_GROUPED {
ctx.notice(me, from.uid, format!("\x02{canonical}\x02 already has the maximum of {MAX_GROUPED} grouped nicks.")); ctx.notice(me, from.uid, t!(ctx, "\x02{canonical}\x02 already has the maximum of {max} grouped nicks.", canonical = canonical, max = MAX_GROUPED));
return; return;
} }
match db.group_nick(from.nick, &canonical) { match db.group_nick(from.nick, &canonical) {
Ok(()) => ctx.notice(me, from.uid, format!("Your nick \x02{}\x02 is now grouped to \x02{canonical}\x02.", from.nick)), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Your nick \x02{nick}\x02 is now grouped to \x02{canonical}\x02.", nick = from.nick, canonical = canonical)),
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."),
} }
} }

View file

@ -1,5 +1,6 @@
use echo_api::{human_time, AuthThen, Store}; use echo_api::{human_time, AuthThen, Store};
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// IDENTIFY [account] <password>: log in. The account defaults to the current // IDENTIFY [account] <password>: log in. The account defaults to the current
// nick, so both the bare-password and account+password forms work. // nick, so both the bare-password and account+password forms work.
@ -14,7 +15,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
// Distinguish an unregistered account from a wrong password. // Distinguish an unregistered account from a wrong password.
if !db.exists(account_name) { if !db.exists(account_name) {
ctx.fail(me, from.uid, "IDENTIFY", "ACCOUNT_NOT_REGISTERED", format!("\x02{account_name}\x02 isn't registered.")); ctx.fail(me, from.uid, "IDENTIFY", "ACCOUNT_NOT_REGISTERED", t!(ctx, "\x02{account_name}\x02 isn't registered.", account_name = account_name));
return; return;
} }
// A suspended account can't be logged into (checked before the password so it // A suspended account can't be logged into (checked before the password so it
@ -23,16 +24,16 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if let Some(acc) = db.resolve_account(account_name).map(str::to_string) { if let Some(acc) = db.resolve_account(account_name).map(str::to_string) {
if db.is_suspended(&acc) { if db.is_suspended(&acc) {
if let Some(s) = db.suspension(&acc) { if let Some(s) = db.suspension(&acc) {
let mut msg = format!("\x02{acc}\x02 is suspended, so you can't log in to it right now. It was suspended by \x02{}\x02 on {}", s.by, human_time(s.ts)); let mut msg = t!(ctx, "\x02{acc}\x02 is suspended, so you can't log in to it right now. It was suspended by \x02{by}\x02 on {when}", acc = acc, by = s.by, when = human_time(s.ts));
if s.reason.trim().is_empty() { if s.reason.trim().is_empty() {
msg.push('.'); msg.push('.');
} else { } else {
msg.push_str(&format!(" — reason: {}", s.reason)); msg.push_str(&t!(ctx, " — reason: {reason}", reason = s.reason));
} }
if let Some(exp) = s.expires { if let Some(exp) = s.expires {
msg.push_str(&format!(" The suspension is due to lift on {}.", human_time(exp))); msg.push_str(&t!(ctx, " The suspension is due to lift on {when}.", when = human_time(exp)));
} }
msg.push_str(" If you think this is a mistake, please contact the network staff."); msg.push_str(&t!(ctx, " If you think this is a mistake, please contact the network staff."));
ctx.fail(me, from.uid, "IDENTIFY", "ACCOUNT_SUSPENDED", msg); ctx.fail(me, from.uid, "IDENTIFY", "ACCOUNT_SUSPENDED", msg);
} }
return; return;
@ -40,7 +41,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
// Refuse while throttled, so a password can't be brute-forced. // Refuse while throttled, so a password can't be brute-forced.
if let Some(secs) = db.auth_lockout(account_name) { if let Some(secs) = db.auth_lockout(account_name) {
ctx.fail(me, from.uid, "IDENTIFY", "RATE_LIMITED", format!("Too many failed attempts. Please wait {secs}s and try again.")); ctx.fail(me, from.uid, "IDENTIFY", "RATE_LIMITED", t!(ctx, "Too many failed attempts. Please wait {secs}s and try again.", secs = secs));
return; return;
} }
// Fetch the verifier cheaply and hand the (~1s) PBKDF2 verify to the engine to // Fetch the verifier cheaply and hand the (~1s) PBKDF2 verify to the engine to
@ -59,7 +60,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
Some((account, verifier)) => { Some((account, verifier)) => {
// Already identified to this account: skip the (wasted) verify. // Already identified to this account: skip the (wasted) verify.
if from.account == Some(account.as_str()) { if from.account == Some(account.as_str()) {
ctx.notice(me, from.uid, format!("You're already identified as \x02{}\x02.", account)); ctx.notice(me, from.uid, t!(ctx, "You're already identified as \x02{account}\x02.", account = account));
return; return;
} }
ctx.defer_authenticate( ctx.defer_authenticate(

View file

@ -1,5 +1,6 @@
use echo_api::{human_time, NetView, Priv, Store}; use echo_api::{human_time, NetView, Priv, Store};
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// INFO [account]: show an account's registration details. The email and other // INFO [account]: show an account's registration details. The email and other
// private fields are shown to the account's own owner, or to an oper with the // private fields are shown to the account's own owner, or to an oper with the
@ -7,13 +8,13 @@ use echo_api::{Sender, ServiceCtx};
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
let name = args.get(1).copied().unwrap_or(from.nick); let name = args.get(1).copied().unwrap_or(from.nick);
let Some(acct) = db.account(name) else { let Some(acct) = db.account(name) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 isn't registered.", name = name));
return; return;
}; };
let is_owner = from.account == Some(acct.name.as_str()); let is_owner = from.account == Some(acct.name.as_str());
let privileged = is_owner || from.privs.has(Priv::Auspex); let privileged = is_owner || from.privs.has(Priv::Auspex);
ctx.notice(me, from.uid, format!("Information for \x02{}\x02:", acct.name)); ctx.notice(me, from.uid, t!(ctx, "Information for \x02{name}\x02:", name = acct.name));
ctx.notice(me, from.uid, format!(" Registered : {}", human_time(acct.ts))); ctx.notice(me, from.uid, t!(ctx, " Registered : {when}", when = human_time(acct.ts)));
// Last-seen is public by default; SET HIDE STATUS keeps it to owner and opers. // Last-seen is public by default; SET HIDE STATUS keeps it to owner and opers.
if privileged || !acct.hide_status { if privileged || !acct.hide_status {
let last_seen = if net.uids_logged_into(&acct.name).is_empty() { let last_seen = if net.uids_logged_into(&acct.name).is_empty() {
@ -21,30 +22,30 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
} else { } else {
"now (online)".to_string() "now (online)".to_string()
}; };
ctx.notice(me, from.uid, format!(" Last seen : {last_seen}")); ctx.notice(me, from.uid, t!(ctx, " Last seen : {last_seen}", last_seen = last_seen));
} }
// A greet is public — the bot shows it in-channel to everyone anyway. // A greet is public — the bot shows it in-channel to everyone anyway.
if !acct.greet.is_empty() { if !acct.greet.is_empty() {
ctx.notice(me, from.uid, format!(" Greet : {}", acct.greet)); ctx.notice(me, from.uid, t!(ctx, " Greet : {greet}", greet = acct.greet));
} }
if privileged { if privileged {
if let Some(s) = db.suspension(&acct.name) { if let Some(s) = db.suspension(&acct.name) {
ctx.notice(me, from.uid, format!(" Suspended : by \x02{}\x02{}", s.by, s.reason)); ctx.notice(me, from.uid, t!(ctx, " Suspended : by \x02{by}\x02 — {reason}", by = s.by, reason = s.reason));
} }
match &acct.email { match &acct.email {
Some(email) if acct.verified => ctx.notice(me, from.uid, format!(" Email : {email}")), Some(email) if acct.verified => ctx.notice(me, from.uid, t!(ctx, " Email : {email}", email = email)),
Some(email) => ctx.notice(me, from.uid, format!(" Email : {email} (unconfirmed)")), Some(email) => ctx.notice(me, from.uid, t!(ctx, " Email : {email} (unconfirmed)", email = email)),
None => ctx.notice(me, from.uid, " Email : (none set)"), None => ctx.notice(me, from.uid, " Email : (none set)"),
} }
let ajoin = db.ajoin_list(&acct.name); let ajoin = db.ajoin_list(&acct.name);
if !ajoin.is_empty() { if !ajoin.is_empty() {
ctx.notice(me, from.uid, format!(" Auto-join : {} channel(s) — see \x02AJOIN LIST\x02", ajoin.len())); ctx.notice(me, from.uid, t!(ctx, " Auto-join : {count} channel(s) — see \x02AJOIN LIST\x02", count = ajoin.len()));
} }
} }
// A staff note is for operators' eyes only, never the account's owner. // A staff note is for operators' eyes only, never the account's owner.
if from.privs.has(Priv::Auspex) { if from.privs.has(Priv::Auspex) {
if let Some(note) = db.account_note(&acct.name) { if let Some(note) = db.account_note(&acct.name) {
ctx.notice(me, from.uid, format!(" Staff note : {note}")); ctx.notice(me, from.uid, t!(ctx, " Staff note : {note}", note = note));
} }
} }
} }

View file

@ -134,7 +134,7 @@ impl Service for NickServ {
Some("LIST") => list::handle(me, from, args, ctx, db), Some("LIST") => list::handle(me, from, args, ctx, db),
Some("GETEMAIL") => getemail::handle(me, from, args, ctx, db), Some("GETEMAIL") => getemail::handle(me, from, 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()),
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), Some(other) => ctx.notice(me, from.uid, echo_api::t!(ctx, "I don't know the command \x02{other}\x02. Try \x02HELP\x02.", other = other)),
None => {} None => {}
} }
} }

View file

@ -1,4 +1,5 @@
use echo_api::{human_time, Priv, Sender, ServiceCtx, Store}; use echo_api::{human_time, Priv, Sender, ServiceCtx, Store};
use echo_api::t;
// A cap so a broad glob can't flood the requesting oper. // A cap so a broad glob can't flood the requesting oper.
const MAX_SHOWN: usize = 100; const MAX_SHOWN: usize = 100;
@ -13,11 +14,11 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
let mut matches = db.accounts_matching(pattern); let mut matches = db.accounts_matching(pattern);
matches.sort_by_key(|a| a.name.to_ascii_lowercase()); matches.sort_by_key(|a| a.name.to_ascii_lowercase());
ctx.notice(me, from.uid, format!("Accounts matching \x02{pattern}\x02:")); ctx.notice(me, from.uid, t!(ctx, "Accounts matching \x02{pattern}\x02:", pattern = pattern));
for a in matches.iter().take(MAX_SHOWN) { for a in matches.iter().take(MAX_SHOWN) {
let flag = if a.verified { "" } else { " (unconfirmed)" }; let flag = if a.verified { "" } else { " (unconfirmed)" };
ctx.notice(me, from.uid, format!(" \x02{}\x02 registered {}{}", a.name, human_time(a.ts), flag)); ctx.notice(me, from.uid, t!(ctx, " \x02{name}\x02 registered {when}{flag}", name = a.name, when = human_time(a.ts), flag = flag));
} }
let more = if matches.len() > MAX_SHOWN { format!(", showing the first {MAX_SHOWN}") } else { String::new() }; let more = if matches.len() > MAX_SHOWN { t!(ctx, ", showing the first {max}", max = MAX_SHOWN) } else { String::new() };
ctx.notice(me, from.uid, format!("End of list — {} match(es){more}.", matches.len())); ctx.notice(me, from.uid, t!(ctx, "End of list — {count} match(es){more}.", count = matches.len(), more = more));
} }

View file

@ -1,4 +1,5 @@
use echo_api::{NetView, Sender, ServiceCtx, Store}; use echo_api::{NetView, Sender, ServiceCtx, Store};
use echo_api::t;
// LOGOUT: log out and rename to a guest nick (prefix + a per-logout sequence). // LOGOUT: log out and rename to a guest nick (prefix + a per-logout sequence).
pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) { pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ctx: &mut ServiceCtx, net: &dyn NetView, db: &dyn Store) {
@ -9,5 +10,5 @@ pub fn handle(me: &str, guest_nick: &str, guest_seq: &mut u32, from: &Sender, ct
let guest = echo_api::next_guest_nick(guest_nick, guest_seq, net, db); let guest = echo_api::next_guest_nick(guest_nick, guest_seq, net, db);
ctx.logout(from.uid); ctx.logout(from.uid);
ctx.force_nick(from.uid, &guest); ctx.force_nick(from.uid, &guest);
ctx.notice(me, from.uid, format!("You're now logged out. Your nick is now \x02{}\x02.", guest)); ctx.notice(me, from.uid, t!(ctx, "You're now logged out. Your nick is now \x02{guest}\x02.", guest = guest));
} }

View file

@ -1,4 +1,5 @@
use echo_api::{Priv, Sender, ServiceCtx, Store}; use echo_api::{Priv, Sender, ServiceCtx, Store};
use echo_api::t;
// NOEXPIRE <account> {ON|OFF}: pin an account so inactivity-expiry never drops // NOEXPIRE <account> {ON|OFF}: pin an account so inactivity-expiry never drops
// it (or lift the pin). Oper-only (Priv::Admin) — protecting a record from // it (or lift the pin). Oper-only (Priv::Admin) — protecting a record from
@ -13,13 +14,13 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
let Some(account) = db.resolve_account(target).map(str::to_string) else { let Some(account) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't registered.", target = target));
return; return;
}; };
match db.set_account_noexpire(&account, on) { match db.set_account_noexpire(&account, on) {
Ok(true) if on => ctx.notice(me, from.uid, format!("\x02{account}\x02 will no longer expire.")), Ok(true) if on => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 will no longer expire.", account = account)),
Ok(true) => ctx.notice(me, from.uid, format!("\x02{account}\x02 can expire from inactivity again.")), Ok(true) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 can expire from inactivity again.", account = account)),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{account}\x02 was already set that way.")), Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 was already set that way.", account = account)),
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."),
} }
} }

View file

@ -1,5 +1,6 @@
use echo_api::{CodeKind, Store}; use echo_api::{CodeKind, Store};
use echo_api::{Sender, ServiceCtx}; use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// RESETPASS <account>: email a reset code to the address on file. // RESETPASS <account>: email a reset code to the address on file.
// RESETPASS <account> <code> <newpassword>: complete the reset with that code. // RESETPASS <account> <code> <newpassword>: complete the reset with that code.
@ -11,7 +12,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
match (args.get(1), args.get(2), args.get(3)) { match (args.get(1), args.get(2), args.get(3)) {
(Some(&name), None, None) => { (Some(&name), None, None) => {
let Some(canonical) = db.resolve_account(name).map(str::to_string) else { let Some(canonical) = db.resolve_account(name).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 isn't registered.", name = name));
return; return;
}; };
let Some(email) = db.account(&canonical).and_then(|a| a.email.clone()) else { let Some(email) = db.account(&canonical).and_then(|a| a.email.clone()) else {
@ -20,17 +21,17 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
let wait = db.code_issue_wait(&canonical); let wait = db.code_issue_wait(&canonical);
if wait > 0 { if wait > 0 {
ctx.notice(me, from.uid, format!("A code was just sent — please wait \x02{wait}\x02s before requesting another.")); ctx.notice(me, from.uid, t!(ctx, "A code was just sent — please wait \x02{wait}\x02s before requesting another.", wait = wait));
return; return;
} }
let code = db.issue_code(&canonical, CodeKind::Reset); let code = db.issue_code(&canonical, CodeKind::Reset);
let mail = echo_api::email::reset(db.email_brand(), db.email_accent(), db.email_logo(), &canonical, &code); let mail = echo_api::email::reset(db.email_brand(), db.email_accent(), db.email_logo(), &canonical, &code);
ctx.send_email(email, mail.subject, mail.text, Some(mail.html)); ctx.send_email(email, mail.subject, mail.text, Some(mail.html));
ctx.notice(me, from.uid, format!("A reset code has been emailed to the address on file for \x02{canonical}\x02.")); ctx.notice(me, from.uid, t!(ctx, "A reset code has been emailed to the address on file for \x02{canonical}\x02.", canonical = canonical));
} }
(Some(&name), Some(&code), Some(&newpass)) => { (Some(&name), Some(&code), Some(&newpass)) => {
let Some(canonical) = db.resolve_account(name).map(str::to_string) else { let Some(canonical) = db.resolve_account(name).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{name}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{name}\x02 isn't registered.", name = name));
return; return;
}; };
if !db.take_code(&canonical, CodeKind::Reset, code) { if !db.take_code(&canonical, CodeKind::Reset, code) {

View file

@ -1,4 +1,5 @@
use echo_api::{Priv, Sender, ServiceCtx, Store}; use echo_api::{Priv, Sender, ServiceCtx, Store};
use echo_api::t;
// SASET <account> <option> [value]: the operator counterpart of SET, editing // SASET <account> <option> [value]: the operator counterpart of SET, editing
// another account's settings. Oper-only (Priv::Admin). Mirrors the self-service // another account's settings. Oper-only (Priv::Admin). Mirrors the self-service
@ -13,7 +14,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return; return;
}; };
let Some(account) = db.resolve_account(target).map(str::to_string) else { let Some(account) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered.")); ctx.notice(me, from.uid, t!(ctx, "\x02{target}\x02 isn't registered.", target = target));
return; return;
}; };
// Credential/identity fields are owned by the website in external mode. // Credential/identity fields are owned by the website in external mode.
@ -38,8 +39,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
let email = args.get(3).map(|s| s.to_string()); let email = args.get(3).map(|s| s.to_string());
let cleared = email.is_none(); let cleared = email.is_none();
match db.set_email(&account, email) { match db.set_email(&account, email) {
Ok(()) if cleared => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 cleared.")), Ok(()) if cleared => ctx.notice(me, from.uid, t!(ctx, "Email for \x02{account}\x02 cleared.", account = account)),
Ok(()) => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 updated.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Email for \x02{account}\x02 updated.", account = account)),
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."),
} }
} }
@ -47,8 +48,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
let greet = if args.len() > 3 { args[3..].join(" ") } else { String::new() }; let greet = if args.len() > 3 { args[3..].join(" ") } else { String::new() };
let cleared = greet.is_empty(); let cleared = greet.is_empty();
match db.set_greet(&account, &greet) { match db.set_greet(&account, &greet) {
Ok(()) if cleared => ctx.notice(me, from.uid, format!("Greet for \x02{account}\x02 cleared.")), Ok(()) if cleared => ctx.notice(me, from.uid, t!(ctx, "Greet for \x02{account}\x02 cleared.", account = account)),
Ok(()) => ctx.notice(me, from.uid, format!("Greet for \x02{account}\x02 is now: {greet}")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Greet for \x02{account}\x02 is now: {greet}", account = account, greet = greet)),
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."),
} }
} }

View file

@ -41,8 +41,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
} }
} }
match db.set_email(account, email) { match db.set_email(account, email) {
Ok(()) if cleared => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 cleared.")), Ok(()) if cleared => ctx.notice(me, from.uid, t!(ctx, "Email for \x02{account}\x02 cleared.", account = account)),
Ok(()) => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 updated.")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Email for \x02{account}\x02 updated.", account = account)),
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."),
} }
} }
@ -75,7 +75,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
Some("STATUS") | Some("USERMASK") => { Some("STATUS") | Some("USERMASK") => {
let Some(on) = args.get(3).and_then(|s| parse_toggle(s)) else { let Some(on) = args.get(3).and_then(|s| parse_toggle(s)) else {
let state = if db.account_hides_status(account) { "ON" } else { "OFF" }; let state = if db.account_hides_status(account) { "ON" } else { "OFF" };
ctx.notice(me, from.uid, format!("HIDE STATUS is \x02{state}\x02. Syntax: SET HIDE STATUS {{ON|OFF}}")); ctx.notice(me, from.uid, t!(ctx, "HIDE STATUS is \x02{state}\x02. Syntax: SET HIDE STATUS {ON|OFF}", state = state));
return; return;
}; };
match db.set_account_hide_status(account, on) { match db.set_account_hide_status(account, on) {
@ -97,7 +97,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}; };
let Some(on) = on else { let Some(on) = on else {
let state = if db.account_wants_protect(account) { "ON" } else { "OFF" }; let state = if db.account_wants_protect(account) { "ON" } else { "OFF" };
ctx.notice(me, from.uid, format!("KILL (nick protection) is \x02{state}\x02. Syntax: SET KILL {{ON|OFF}}")); ctx.notice(me, from.uid, t!(ctx, "KILL (nick protection) is \x02{state}\x02. Syntax: SET KILL {ON|OFF}", state = state));
return; return;
}; };
match db.set_account_kill(account, on) { match db.set_account_kill(account, on) {
@ -109,7 +109,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
Some("AUTOOP") => { Some("AUTOOP") => {
let Some(on) = args.get(2).and_then(|s| parse_toggle(s)) else { let Some(on) = args.get(2).and_then(|s| parse_toggle(s)) else {
let state = if db.account_wants_autoop(account) { "ON" } else { "OFF" }; let state = if db.account_wants_autoop(account) { "ON" } else { "OFF" };
ctx.notice(me, from.uid, format!("AUTOOP is \x02{state}\x02. Syntax: SET AUTOOP {{ON|OFF}}")); ctx.notice(me, from.uid, t!(ctx, "AUTOOP is \x02{state}\x02. Syntax: SET AUTOOP {ON|OFF}", state = state));
return; return;
}; };
match db.set_account_autoop(account, on) { match db.set_account_autoop(account, on) {
@ -121,7 +121,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
Some("SNOTICE") | Some("SNOTICES") => { Some("SNOTICE") | Some("SNOTICES") => {
let Some(on) = args.get(2).and_then(|s| parse_toggle(s)) else { let Some(on) = args.get(2).and_then(|s| parse_toggle(s)) else {
let state = if db.account_wants_snotice(account) { "ON" } else { "OFF" }; let state = if db.account_wants_snotice(account) { "ON" } else { "OFF" };
ctx.notice(me, from.uid, format!("SNOTICE is \x02{state}\x02. Syntax: SET SNOTICE {{ON|OFF}}")); ctx.notice(me, from.uid, t!(ctx, "SNOTICE is \x02{state}\x02. Syntax: SET SNOTICE {ON|OFF}", state = state));
return; return;
}; };
match db.set_account_snotice(account, on) { match db.set_account_snotice(account, on) {
@ -136,7 +136,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
let cleared = greet.is_empty(); let cleared = greet.is_empty();
match db.set_greet(account, &greet) { match db.set_greet(account, &greet) {
Ok(()) if cleared => ctx.notice(me, from.uid, "Your greet has been cleared."), Ok(()) if cleared => ctx.notice(me, from.uid, "Your greet has been cleared."),
Ok(()) => ctx.notice(me, from.uid, format!("Your greet is now: {greet}")), Ok(()) => ctx.notice(me, from.uid, t!(ctx, "Your greet is now: {greet}", greet = greet)),
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 files were not shown because too many files have changed in this diff Show more