MemoServ: add CANCEL, CHECK, and INFO
All checks were successful
CI / check (push) Successful in 3m49s

CANCEL recalls the sender's most recent unread memo to a target (one the
recipient already read can't be recalled); CHECK reports whether the last
memo you sent someone has been read; INFO summarises your mailbox. Adds
memo_cancel/memo_check store methods and shares MAX_MEMOS from the module
root instead of duplicating it in send.rs.
This commit is contained in:
Jean Chevronnet 2026-07-15 18:09:33 +00:00
parent a26ee722d1
commit 98ecbca414
No known key found for this signature in database
9 changed files with 118 additions and 2 deletions

View file

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

View file

@ -0,0 +1,18 @@
use echo_api::{human_time, Sender, ServiceCtx, Store};
// 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) {
let Some(&target) = args.get(1) else {
ctx.notice(me, from.uid, "Syntax: CHECK <nick>");
return;
};
let Some(dest) = db.resolve_account(target).map(str::to_string) else {
ctx.notice(me, from.uid, format!("\x02{target}\x02 isn't registered."));
return;
};
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((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))),
None => ctx.notice(me, from.uid, format!("You have no memo on record to \x02{target}\x02.")),
}
}

View file

@ -0,0 +1,12 @@
use echo_api::{Sender, ServiceCtx, Store};
// 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) {
let total = db.memo_list(account).len();
let unread = db.unread_memos(account);
ctx.notice(
me,
from.uid,
format!("You have \x02{total}\x02 memo(s), \x02{unread}\x02 unread, of a maximum \x02{}\x02.", super::MAX_MEMOS),
);
}

View file

@ -14,14 +14,26 @@ mod list;
mod read;
#[path = "del.rs"]
mod del;
#[path = "cancel.rs"]
mod cancel;
#[path = "check.rs"]
mod check;
#[path = "info.rs"]
mod info;
const BLURB: &str = "MemoServ delivers messages to registered users, online or not. You must be identified to use it.";
// A full mailbox rejects new memos, so nobody can be flooded.
pub(crate) const MAX_MEMOS: usize = 30;
const TOPICS: &[HelpEntry] = &[
HelpEntry { cmd: "SEND", summary: "leave a memo for an account", detail: "Syntax: \x02SEND <nick> <text>\x02\nLeaves a memo on a registered account's mailbox." },
HelpEntry { cmd: "LIST", summary: "list your memos", detail: "Syntax: \x02LIST\x02\nLists the memos in your mailbox." },
HelpEntry { cmd: "READ", summary: "read a memo", detail: "Syntax: \x02READ <num>|NEW|ALL\x02\nReads a memo by number, your unread ones, or all of them." },
HelpEntry { cmd: "DEL", summary: "delete a memo", detail: "Syntax: \x02DEL <num>|ALL\x02\nDeletes a memo by number, or your whole mailbox." },
HelpEntry { cmd: "CANCEL", summary: "recall a memo you sent", detail: "Syntax: \x02CANCEL <nick>\x02\nRecalls the last memo you sent them, as long as they haven't read it yet." },
HelpEntry { cmd: "CHECK", summary: "see if a memo was read", detail: "Syntax: \x02CHECK <nick>\x02\nReports whether the last memo you sent them has been read." },
HelpEntry { cmd: "INFO", summary: "your mailbox summary", detail: "Syntax: \x02INFO\x02\nShows how many memos you have, how many are unread, and your capacity." },
];
pub struct MemoServ {
@ -60,6 +72,9 @@ impl Service for MemoServ {
Some("LIST") => list::handle(me, from, account, ctx, db),
Some("READ") => read::handle(me, from, account, args, ctx, db),
Some("DEL") | Some("DELETE") => del::handle(me, from, account, args, ctx, db),
Some("CANCEL") => cancel::handle(me, from, account, args, ctx, db),
Some("CHECK") => check::handle(me, from, account, args, ctx, db),
Some("INFO") => info::handle(me, from, account, ctx, db),
Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")),
None => {}
}

View file

@ -1,7 +1,6 @@
use echo_api::{Sender, ServiceCtx, Store};
// A full mailbox rejects new memos, so nobody can be flooded.
const MAX_MEMOS: usize = 30;
use super::MAX_MEMOS;
// SEND <nick> <text>: leave a memo on a registered account's mailbox.
pub fn handle(me: &str, from: &Sender, account: &str, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) {