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,5 +1,6 @@
use echo_api::Store;
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.
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("");
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;
}
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(false) => ctx.notice(me, from.uid, format!("Updated the key for \x02{channel}\x02 on 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, 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."),
}
}
@ -38,8 +39,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
};
match db.ajoin_del(account, channel) {
Ok(true) => ctx.notice(me, from.uid, format!("Removed \x02{channel}\x02 from your auto-join list.")),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{channel}\x02 isn't on 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, 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."),
}
}
@ -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.");
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 {
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 {
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::t;
// 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) {
@ -21,8 +22,8 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
return;
}
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 {
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::{Sender, ServiceCtx};
use echo_api::t;
// CERT ADD|DEL|LIST [password] [fingerprint]: manage the TLS certificate
// 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() {
ctx.notice(me, from.uid, "No certificate fingerprints are registered to your account.");
} 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") => {
@ -28,7 +29,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
};
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::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."),
@ -44,7 +45,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
};
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."),
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.
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) {
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;
}
match db.authenticate(from.nick, password).map(str::to_string) {

View file

@ -1,5 +1,6 @@
use echo_api::{CodeKind, Store};
use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// 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) {
@ -12,7 +13,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
};
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;
}
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;
}
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."),
}
}

View file

@ -1,6 +1,7 @@
use echo_api::Store;
use echo_api::{Sender, ServiceCtx};
use echo_api::NetView;
use echo_api::t;
// DROP <password>: delete your account. Re-authenticates as confirmation, releases
// 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
// spammed to stall the engine, and record the attempt.
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;
}
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) {
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() {
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() {
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::t;
// 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) {
@ -12,8 +13,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
};
let accounts = db.accounts_by_email(pattern);
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;
}
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::{Sender, ServiceCtx};
use echo_api::NetView;
use echo_api::t;
// 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.
@ -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) {
let cmd = if regain { "RECOVER" } else { "GHOST" };
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;
};
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;
};
// 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
} else if let Some(&pw) = args.get(2) {
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;
}
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
};
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;
}
// 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) => {
let guest = echo_api::next_guest_nick(guest_nick, guest_seq, net, &*db);
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 => {
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;
}
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).
if regain {
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::{Sender, ServiceCtx};
use echo_api::t;
// GLIST: list the nicks grouped to your account.
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);
nicks.sort();
ctx.notice(me, from.uid, format!("Nicks grouped to \x02{account}\x02:"));
ctx.notice(me, from.uid, format!(" \x02{account}\x02 (main)"));
ctx.notice(me, from.uid, t!(ctx, "Nicks grouped to \x02{account}\x02:", account = account));
ctx.notice(me, from.uid, t!(ctx, " \x02{account}\x02 (main)", account = account));
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::{Sender, ServiceCtx};
use echo_api::t;
// GROUP <account> <password>: link your current nick to an existing account, so
// 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
// engine). Also feeds the auth audit feed.
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;
}
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);
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;
}
// 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() {
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;
}
const MAX_GROUPED: usize = 25;
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;
}
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."),
}
}

View file

@ -1,5 +1,6 @@
use echo_api::{human_time, AuthThen, Store};
use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// IDENTIFY [account] <password>: log in. The account defaults to the current
// 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.
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;
}
// 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 db.is_suspended(&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() {
msg.push('.');
} else {
msg.push_str(&format!(" — reason: {}", s.reason));
msg.push_str(&t!(ctx, " — reason: {reason}", reason = s.reason));
}
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);
}
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.
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;
}
// 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)) => {
// Already identified to this account: skip the (wasted) verify.
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;
}
ctx.defer_authenticate(

View file

@ -1,5 +1,6 @@
use echo_api::{human_time, NetView, Priv, Store};
use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// 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
@ -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) {
let name = args.get(1).copied().unwrap_or(from.nick);
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;
};
let is_owner = from.account == Some(acct.name.as_str());
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, format!(" Registered : {}", human_time(acct.ts)));
ctx.notice(me, from.uid, t!(ctx, "Information for \x02{name}\x02:", name = acct.name));
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.
if privileged || !acct.hide_status {
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 {
"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.
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 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 {
Some(email) if acct.verified => ctx.notice(me, from.uid, format!(" Email : {email}")),
Some(email) => ctx.notice(me, from.uid, format!(" Email : {email} (unconfirmed)")),
Some(email) if acct.verified => ctx.notice(me, from.uid, t!(ctx, " Email : {email}", email = email)),
Some(email) => ctx.notice(me, from.uid, t!(ctx, " Email : {email} (unconfirmed)", email = email)),
None => ctx.notice(me, from.uid, " Email : (none set)"),
}
let ajoin = db.ajoin_list(&acct.name);
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.
if from.privs.has(Priv::Auspex) {
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("GETEMAIL") => getemail::handle(me, from, args, ctx, db),
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 => {}
}
}

View file

@ -1,4 +1,5 @@
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.
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);
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) {
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() };
ctx.notice(me, from.uid, format!("End of list — {} match(es){more}.", matches.len()));
let more = if matches.len() > MAX_SHOWN { t!(ctx, ", showing the first {max}", max = MAX_SHOWN) } else { String::new() };
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::t;
// 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) {
@ -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);
ctx.logout(from.uid);
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::t;
// 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
@ -13,13 +14,13 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
};
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;
};
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) => ctx.notice(me, from.uid, format!("\x02{account}\x02 can expire from inactivity again.")),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{account}\x02 was already set that way.")),
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, t!(ctx, "\x02{account}\x02 can expire from inactivity again.", account = account)),
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."),
}
}

View file

@ -1,5 +1,6 @@
use echo_api::{CodeKind, Store};
use echo_api::{Sender, ServiceCtx};
use echo_api::t;
// RESETPASS <account>: email a reset code to the address on file.
// 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)) {
(Some(&name), None, None) => {
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;
};
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);
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;
}
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);
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)) => {
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;
};
if !db.take_code(&canonical, CodeKind::Reset, code) {

View file

@ -1,4 +1,5 @@
use echo_api::{Priv, Sender, ServiceCtx, Store};
use echo_api::t;
// SASET <account> <option> [value]: the operator counterpart of SET, editing
// 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;
};
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;
};
// 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 cleared = email.is_none();
match db.set_email(&account, email) {
Ok(()) if cleared => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 cleared.")),
Ok(()) => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 updated.")),
Ok(()) if cleared => ctx.notice(me, from.uid, t!(ctx, "Email for \x02{account}\x02 cleared.", account = account)),
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."),
}
}
@ -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 cleared = greet.is_empty();
match db.set_greet(&account, &greet) {
Ok(()) if cleared => ctx.notice(me, from.uid, format!("Greet for \x02{account}\x02 cleared.")),
Ok(()) => ctx.notice(me, from.uid, format!("Greet for \x02{account}\x02 is now: {greet}")),
Ok(()) if cleared => ctx.notice(me, from.uid, t!(ctx, "Greet for \x02{account}\x02 cleared.", account = account)),
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."),
}
}

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) {
Ok(()) if cleared => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 cleared.")),
Ok(()) => ctx.notice(me, from.uid, format!("Email for \x02{account}\x02 updated.")),
Ok(()) if cleared => ctx.notice(me, from.uid, t!(ctx, "Email for \x02{account}\x02 cleared.", account = account)),
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."),
}
}
@ -75,7 +75,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
Some("STATUS") | Some("USERMASK") => {
let Some(on) = args.get(3).and_then(|s| parse_toggle(s)) else {
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;
};
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 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;
};
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") => {
let Some(on) = args.get(2).and_then(|s| parse_toggle(s)) else {
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;
};
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") => {
let Some(on) = args.get(2).and_then(|s| parse_toggle(s)) else {
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;
};
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();
match db.set_greet(account, &greet) {
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."),
}
}

View file

@ -1,4 +1,5 @@
use echo_api::{parse_duration, NetView, Priv, Sender, ServiceCtx, Store};
use echo_api::t;
use std::time::{SystemTime, UNIX_EPOCH};
// SUSPEND <account> [+expiry] [reason] / UNSUSPEND <account>: freeze or unfreeze
@ -15,14 +16,14 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
return;
};
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;
};
if !suspending {
match db.unsuspend_account(&account) {
Ok(true) => ctx.notice(me, from.uid, format!("\x02{account}\x02 is no longer suspended.")),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{account}\x02 isn't suspended.")),
Ok(true) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 is no longer suspended.", account = account)),
Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 isn't suspended.", account = account)),
Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."),
}
return;
@ -42,8 +43,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net:
for uid in net.uids_logged_into(&account) {
ctx.logout(&uid);
}
let expiry = if expires.is_some() { " (with expiry)" } else { "" };
ctx.notice(me, from.uid, format!("\x02{account}\x02 is now suspended{expiry}."));
let expiry = if expires.is_some() { t!(ctx, " (with expiry)") } else { String::new() };
ctx.notice(me, from.uid, t!(ctx, "\x02{account}\x02 is now suspended{expiry}.", account = account, expiry = expiry));
}
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::{Sender, ServiceCtx};
use echo_api::t;
// UNGROUP [nick]: remove a nick grouped to your account (defaults to your current nick).
pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {
@ -13,12 +14,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
return;
}
if db.resolve_account(nick).is_none_or(|a| !a.eq_ignore_ascii_case(account)) {
ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't grouped to your account."));
ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 isn't grouped to your account.", nick = nick));
return;
}
match db.ungroup_nick(nick) {
Ok(true) => ctx.notice(me, from.uid, format!("\x02{nick}\x02 is no longer grouped to \x02{account}\x02.")),
Ok(false) => ctx.notice(me, from.uid, format!("\x02{nick}\x02 isn't a grouped nick.")),
Ok(true) => ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 is no longer grouped to \x02{account}\x02.", nick = nick, account = account)),
Ok(false) => ctx.notice(me, from.uid, t!(ctx, "\x02{nick}\x02 isn't a grouped nick.", nick = nick)),
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::{Sender, ServiceCtx, Store};
use echo_api::t;
// UPDATE: re-apply your account's auto-joins and vhost, and re-check for waiting
// memos, without having to re-identify.
@ -15,7 +16,7 @@ pub fn handle(me: &str, from: &Sender, ctx: &mut ServiceCtx, db: &dyn Store) {
}
let unread = db.unread_memos(account);
if unread > 0 {
ctx.notice(me, from.uid, format!("You have \x02{unread}\x02 new memo(s). Read them with \x02/msg MemoServ READ NEW\x02."));
ctx.notice(me, from.uid, t!(ctx, "You have \x02{unread}\x02 new memo(s). Read them with \x02/msg MemoServ READ NEW\x02.", unread = unread));
}
ctx.notice(me, from.uid, "Your status has been refreshed.");
}