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

@ -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
// 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.
let is_admin = from.privs.has(Priv::Admin);
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;
}
if !assigning {
match db.unassign_bot(chan) {
Ok(true) => ctx.notice(me, from.uid, format!("The bot has left \x02{chan}\x02.")),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no bot assigned.")),
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, 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."),
}
return;
@ -30,16 +30,16 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
};
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;
};
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;
}
let botnick = target.nick;
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."),
}
}

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
// 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 {
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."),
}
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.
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;
};
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."),
}
}

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
// 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(" ");
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."),
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),
}
}
@ -34,30 +34,30 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
}
let pattern = args[3..].join(" ");
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."),
Err(_) => reg_error(me, from, chan, ctx),
}
}
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),
},
None | Some("LIST") => {
let words = db.badwords(chan);
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;
}
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() {
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) {
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 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() };
match db.bot_add(nick, user, host, &gecos) {
Ok(()) => ctx.notice(me, from.uid, format!("Bot \x02{nick}\x02 (\x02{user}@{host}\x02) added.")),
Err(_) => ctx.notice(me, from.uid, format!("A bot named \x02{nick}\x02 already exists, or that didn't work.")),
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, t!(ctx, "A bot named \x02{nick}\x02 already exists, or that didn't work.", nick = nick)),
}
}
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.
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;
};
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 gecos = if args.len() > 6 { args[6..].join(" ") } else { cur.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).")),
Err(ChanError::Exists) => ctx.notice(me, from.uid, format!("A bot named \x02{newnick}\x02 already exists.")),
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, 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."),
}
}
@ -45,14 +45,14 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
};
if nick == "*" {
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."),
}
return;
}
match db.bot_del(nick) {
Ok(true) => ctx.notice(me, from.uid, format!("Bot \x02{nick}\x02 deleted.")),
Ok(false) => ctx.notice(me, from.uid, format!("There's no bot named \x02{nick}\x02.")),
Ok(true) => ctx.notice(me, from.uid, t!(ctx, "Bot \x02{nick}\x02 deleted.", nick = nick)),
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."),
}
}
@ -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.");
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 {
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,
// 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.");
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 {
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.");
}

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,
// 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;
}
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."),
}
}

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 <#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('#') {
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;
};
match &chan.assigned_bot {
Some(bot) => ctx.notice(me, from.uid, format!("\x02{target}\x02 is served by bot \x02{bot}\x02.")),
None => ctx.notice(me, from.uid, format!("\x02{target}\x02 has no bot assigned. Assign one with \x02ASSIGN\x02 {target} <bot>.")),
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, 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.
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");
}
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;
}
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;
};
let channels: Vec<String> = db
@ -45,11 +45,11 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
.collect();
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, format!(" Real name: {}", bot.gecos));
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, t!(ctx, " Real name: {gecos}", gecos = bot.gecos));
if channels.is_empty() {
ctx.notice(me, from.uid, " Not assigned to any channel.");
} 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.
// 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") {
match args.get(3).and_then(|s| s.parse::<u16>().ok()) {
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(()) => ctx.notice(me, from.uid, format!("The bot will ban a user after \x02{n}\x02 kick(s) 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, 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),
},
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(" ");
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)."),
}
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 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) {
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),
}
} 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 secs = args.get(5).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
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),
}
} else {
@ -80,7 +80,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
if on {
let times = args.get(4).and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
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),
}
} else {
@ -100,7 +100,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
"DONTKICKOPS" => Kicker::DontKickOps,
"DONTKICKVOICES" => Kicker::DontKickVoices,
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;
}
};
@ -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) {
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 => ctx.notice(me, from.uid, format!("The bot may again kick channel operators in \x02{chan}\x02.")),
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 => ctx.notice(me, from.uid, format!("The bot may again kick voiced users in \x02{chan}\x02.")),
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 => ctx.notice(me, from.uid, format!("The bot will kick without warning in \x02{chan}\x02.")),
Ok(()) if on => ctx.notice(me, from.uid, format!("The \x02{}\x02 kicker is now on in \x02{chan}\x02.", label(kicker))),
Ok(()) => ctx.notice(me, from.uid, format!("The \x02{}\x02 kicker is now off in \x02{chan}\x02.", label(kicker))),
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, 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, 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, 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, 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, t!(ctx, "The bot will kick without warning in \x02{chan}\x02.", chan = chan)),
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, 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),
}
}
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;
//! 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"]
mod bot;
@ -31,11 +31,11 @@ mod trigger;
// 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 {
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;
};
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;
}
true
@ -95,7 +95,7 @@ impl Service for BotServ {
Some("COPY") => copy::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(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.
// 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) {
let verb = if action { "ACT" } else { "SAY" };
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;
}
let chan = args[1];
let text = args[2..].join(" ");
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;
};
let allowed = from.privs.has(Priv::Admin) || from.account.is_some_and(|a| info.is_op(a));
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;
}
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;
};
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;
};
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) —
// 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;
}
(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;
}
};
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) => ctx.notice(me, from.uid, format!("Bot \x02{target}\x02 is now public.")),
Ok(false) => ctx.notice(me, from.uid, format!("There's no bot named \x02{target}\x02.")),
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, t!(ctx, "Bot \x02{nick}\x02 is now public.", nick = target)),
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."),
}
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") {
match args.get(3).and_then(|s| s.parse::<u16>().ok()) {
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(()) => 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(()) 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, 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."),
},
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) {
Some(s) => s.min(u32::MAX as u64) as u32,
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;
}
}
};
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(()) => ctx.notice(me, from.uid, format!("Kicker bans in \x02{chan}\x02 will expire after \x02{arg}\x02.")),
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, 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."),
}
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() {
"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(()) => ctx.notice(me, from.uid, format!("Greet messages are now \x02off\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, 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."),
},
"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(()) => ctx.notice(me, from.uid, format!("The founder may (un)assign a bot in \x02{chan}\x02 again.")),
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, 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."),
},
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
// 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;
}
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."),
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),
}
}
@ -47,31 +47,31 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
};
match db.trigger_del(chan, n) {
Ok(true) => ctx.notice(me, from.uid, format!("Trigger #\x02{n}\x02 removed from \x02{chan}\x02.")),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{chan}\x02 has no trigger #\x02{n}\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, t!(ctx, "\x02{chan}\x02 has no trigger #\x02{n}\x02.", chan = chan, n = n)),
Err(_) => reg_error(me, from, chan, ctx),
}
}
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),
},
None | Some("LIST") => {
let triggers = db.triggers(chan);
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;
}
ctx.notice(me, from.uid, format!("Triggers for \x02{chan}\x02 ({}):", triggers.len()));
for (i, t) in triggers.iter().enumerate() {
let cd = if t.cooldown > 0 { format!(" (cooldown {}s)", t.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, "Triggers for \x02{chan}\x02 ({count}):", chan = chan, count = triggers.len()));
for (i, tr) in triggers.iter().enumerate() {
let cd = if tr.cooldown > 0 { t!(ctx, " (cooldown {secs}s)", secs = tr.cooldown) } else { String::new() };
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) {
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));
}