diff --git a/api/src/lib.rs b/api/src/lib.rs index 971309d..8db2df6 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1150,7 +1150,7 @@ pub fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx, blurb: &str, topics: } ctx.notice(me, from.uid, "Type \x02HELP \x02 for detail on one command."); } - Some(t) => match topics.iter().find(|e| e.cmd.eq_ignore_ascii_case(t)) { + Some(t) => match topics.iter().find(|e| e.cmd.split('/').any(|c| c.eq_ignore_ascii_case(t))) { Some(e) => { for line in e.detail.lines() { ctx.notice(me, from.uid, line); @@ -1214,3 +1214,62 @@ mod tests { assert!(!Privs::default().any(), "empty set is not an oper"); } } + +#[cfg(test)] +mod help_tests { + use super::*; + + fn texts(ctx: &ServiceCtx) -> Vec { + ctx.actions + .iter() + .filter_map(|a| match a { + NetAction::Notice { text, .. } => Some(text.clone()), + _ => None, + }) + .collect() + } + + const T: &[HelpEntry] = &[ + HelpEntry { cmd: "REGISTER", summary: "register your nick", detail: "Syntax: REGISTER \nRegisters your nick." }, + HelpEntry { cmd: "OP/DEOP", summary: "give or take op", detail: "Syntax: OP <#chan>\nGives or takes op." }, + ]; + + fn sender<'a>() -> Sender<'a> { + Sender { uid: "u1", nick: "nick", account: None, privs: Privs::default() } + } + + #[test] + fn overview_lists_every_command() { + let mut ctx = ServiceCtx::default(); + help("Svc", &sender(), &mut ctx, "the blurb", T, None); + let t = texts(&ctx); + assert_eq!(t[0], "the blurb"); + assert!(t.iter().any(|l| l.contains("REGISTER") && l.contains("register your nick"))); + assert!(t.iter().any(|l| l.contains("OP/DEOP"))); + assert!(t.last().unwrap().contains("HELP ")); + } + + #[test] + fn topic_shows_detail_lines_only() { + let mut ctx = ServiceCtx::default(); + help("Svc", &sender(), &mut ctx, "the blurb", T, Some("register")); + let t = texts(&ctx); + assert!(t[0].contains("Syntax: REGISTER")); + assert!(t.iter().any(|l| l == "Registers your nick.")); + assert!(!t.iter().any(|l| l == "the blurb")); + } + + #[test] + fn topic_matches_a_slash_alias() { + let mut ctx = ServiceCtx::default(); + help("Svc", &sender(), &mut ctx, "b", T, Some("DEOP")); + assert!(texts(&ctx)[0].contains("Syntax: OP")); + } + + #[test] + fn unknown_topic_reports_no_help() { + let mut ctx = ServiceCtx::default(); + help("Svc", &sender(), &mut ctx, "b", T, Some("nope")); + assert!(texts(&ctx)[0].contains("No help")); + } +} diff --git a/modules/botserv/src/lib.rs b/modules/botserv/src/lib.rs index 9ed5e57..71c6c40 100644 --- a/modules/botserv/src/lib.rs +++ b/modules/botserv/src/lib.rs @@ -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::{NetView, Priv, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Priv, Sender, Service, ServiceCtx, Store}; #[path = "bot.rs"] mod bot; @@ -37,6 +37,22 @@ fn require_channel_admin(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceC true } +const BLURB: &str = "BotServ keeps service bots for your channels: assign a bot, speak through it, and configure kickers, badwords, and triggers."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "ASSIGN", summary: "put a bot in a channel", detail: "Syntax: \x02ASSIGN <#channel> \x02\nPuts a service bot in your channel." }, + HelpEntry { cmd: "UNASSIGN", summary: "remove a channel's bot", detail: "Syntax: \x02UNASSIGN <#channel>\x02\nRemoves the assigned bot from your channel." }, + HelpEntry { cmd: "INFO", summary: "show bot/channel settings", detail: "Syntax: \x02INFO \x02\nShows a bot's details or a channel's bot settings." }, + HelpEntry { cmd: "SAY", summary: "speak through the bot", detail: "Syntax: \x02SAY <#channel> \x02\nSays a line as the channel's bot." }, + HelpEntry { cmd: "ACT", summary: "action through the bot", detail: "Syntax: \x02ACT <#channel> \x02\nSends a /me action as the channel's bot." }, + HelpEntry { cmd: "SET", summary: "configure bot behavior", detail: "Syntax: \x02SET <#channel> \x02, or \x02SET PRIVATE \x02\nConfigures a channel's bot behavior, or marks a bot private." }, + HelpEntry { cmd: "KICK", summary: "configure kickers", detail: "Syntax: \x02KICK <#channel> [params]\x02, or \x02KICK <#channel> TTB | TEST \x02\nConfigures a channel's automatic kickers." }, + HelpEntry { cmd: "BADWORDS", summary: "manage badword patterns", detail: "Syntax: \x02BADWORDS <#channel> ADD|DEL|LIST|CLEAR [pattern]\x02\nManages the channel's badword patterns." }, + HelpEntry { cmd: "TRIGGER", summary: "manage auto-responses", detail: "Syntax: \x02TRIGGER <#channel> ADD |[|] | DEL | LIST | CLEAR\x02\nManages the channel's auto-response triggers." }, + HelpEntry { cmd: "COPY", summary: "clone bot settings", detail: "Syntax: \x02COPY <#source> <#dest>\x02\nClones a channel's bot settings to another channel." }, + HelpEntry { cmd: "BOT", summary: "manage the bots (operator)", detail: "Syntax: \x02BOT ADD|CHANGE|DEL|LIST\x02\nCreates and manages the service bots themselves. Operators only." }, +]; + pub struct BotServ { pub uid: String, } @@ -66,7 +82,7 @@ impl Service for BotServ { Some("BADWORDS") => badwords::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("HELP") | None => ctx.notice(me, from.uid, "BotServ keeps service bots for your channels: \x02ASSIGN\x02 <#channel> puts a bot in your channel, \x02UNASSIGN\x02 <#channel> removes it, \x02INFO\x02 shows details, \x02SAY\x02/\x02ACT\x02 <#channel> speaks through the bot, \x02SET\x02 <#channel> GREET toggles greets, \x02KICK\x02 <#channel> configures kickers (with \x02TEST\x02/\x02WARN\x02/\x02TTB\x02), \x02BADWORDS\x02 and \x02TRIGGER\x02 <#channel> ADD|DEL|LIST manage badword regexes and auto-responses, \x02COPY\x02 <#src> <#dst> clones settings. Operators also have \x02BOT\x02 ADD|CHANGE|DEL|LIST."), + 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.")), } } diff --git a/modules/chanserv/src/lib.rs b/modules/chanserv/src/lib.rs index 3caa267..823e6b1 100644 --- a/modules/chanserv/src/lib.rs +++ b/modules/chanserv/src/lib.rs @@ -1,5 +1,5 @@ use echo_api::{ChanError, ChannelView, Priv, Store}; -use echo_api::{Sender, Service, ServiceCtx}; +use echo_api::{HelpEntry, Sender, Service, ServiceCtx}; use echo_api::NetView; #[path = "mode.rs"] @@ -43,6 +43,35 @@ mod xop; mod suspend; mod noexpire; +const BLURB: &str = "ChanServ registers and looks after channels. Register a channel you hold ops in, then manage its access, modes, and topic."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "REGISTER", summary: "register a channel", detail: "Syntax: \x02REGISTER <#channel>\x02\nRegisters a channel to you. You must currently hold ops in it." }, + HelpEntry { cmd: "INFO", summary: "show channel information", detail: "Syntax: \x02INFO <#channel>\x02\nShows a channel's registration and settings." }, + HelpEntry { cmd: "LIST", summary: "list registered channels", detail: "Syntax: \x02LIST\x02\nLists registered channels." }, + HelpEntry { cmd: "SET", summary: "change founder or settings", detail: "Syntax: \x02SET <#channel> FOUNDER | DESC | SIGNKICK|PRIVATE|PEACE|SECUREOPS|KEEPTOPIC|TOPICLOCK {ON|OFF}\x02\nTransfers the founder or changes a channel setting." }, + HelpEntry { cmd: "ACCESS", summary: "manage the access list", detail: "Syntax: \x02ACCESS <#channel> LIST | ADD | DEL \x02\nManages the channel access list." }, + HelpEntry { cmd: "FLAGS", summary: "granular per-account flags", detail: "Syntax: \x02FLAGS <#channel> [account [+/-flags]]\x02\nViews or changes granular per-account channel flags." }, + HelpEntry { cmd: "AOP/SOP/VOP", summary: "tiered access shortcuts", detail: "Syntax: \x02AOP|SOP|VOP <#channel> ADD | DEL | LIST\x02\nTiered shortcuts over ACCESS: AOP and SOP grant op, VOP grants voice." }, + HelpEntry { cmd: "STATUS", summary: "show a user's access", detail: "Syntax: \x02STATUS <#channel> [nick]\x02\nShows a user's access level on a channel." }, + HelpEntry { cmd: "OP/DEOP/VOICE/DEVOICE", summary: "give or take op/voice", detail: "Syntax: \x02OP|DEOP|VOICE|DEVOICE <#channel> [nick]\x02\nGives or takes channel op or voice." }, + HelpEntry { cmd: "KICK", summary: "kick from the channel", detail: "Syntax: \x02KICK <#channel> [reason]\x02\nKicks a user from the channel." }, + HelpEntry { cmd: "BAN/UNBAN", summary: "ban or unban a user", detail: "Syntax: \x02BAN <#channel> [reason]\x02, \x02UNBAN <#channel> [nick]\x02\nBans or unbans a user by host." }, + HelpEntry { cmd: "AKICK", summary: "manage the auto-kick list", detail: "Syntax: \x02AKICK <#channel> ADD [reason] | DEL | LIST\x02\nManages the auto-kick list; matches are banned and kicked on join." }, + HelpEntry { cmd: "ENFORCE", summary: "re-apply lock and access", detail: "Syntax: \x02ENFORCE <#channel>\x02\nRe-applies the mode-lock, access, and akick list to everyone present." }, + HelpEntry { cmd: "TOPIC", summary: "set the topic", detail: "Syntax: \x02TOPIC <#channel> \x02\nSets the channel topic." }, + HelpEntry { cmd: "ENTRYMSG", summary: "message on join", detail: "Syntax: \x02ENTRYMSG <#channel> [CLEAR | ]\x02\nSets a message noticed to users as they join." }, + HelpEntry { cmd: "INVITE", summary: "invite into the channel", detail: "Syntax: \x02INVITE <#channel> [nick]\x02\nInvites you, or a nick, into the channel." }, + HelpEntry { cmd: "GETKEY", summary: "show the channel key", detail: "Syntax: \x02GETKEY <#channel>\x02\nShows the channel key (+k), if one is set." }, + HelpEntry { cmd: "SEEN", summary: "when a nick was last seen", detail: "Syntax: \x02SEEN \x02\nShows when a nick was last seen." }, + HelpEntry { cmd: "CLONE", summary: "copy channel settings", detail: "Syntax: \x02CLONE \x02\nCopies one channel's settings to another you own." }, + HelpEntry { cmd: "MODE", summary: "set channel modes", detail: "Syntax: \x02MODE <#channel> \x02\nSets modes on the channel, e.g. MODE #chan +nt." }, + HelpEntry { cmd: "MLOCK", summary: "lock channel modes", detail: "Syntax: \x02MLOCK <#channel> [modes]\x02\nLocks modes set or unset, e.g. MLOCK #chan +nt-s." }, + HelpEntry { cmd: "DROP", summary: "delete the registration", detail: "Syntax: \x02DROP <#channel>\x02\nDeletes the channel registration." }, + HelpEntry { cmd: "SUSPEND/UNSUSPEND", summary: "suspend a channel (operator)", detail: "Syntax: \x02SUSPEND <#channel> [+expiry] [reason]\x02, \x02UNSUSPEND <#channel>\x02\nSuspends or unsuspends a channel. Operators only." }, + HelpEntry { cmd: "NOEXPIRE", summary: "pin against expiry (operator)", detail: "Syntax: \x02NOEXPIRE <#channel> {ON|OFF}\x02\nPins a channel so inactivity expiry never drops it. Operators only." }, +]; + pub struct ChanServ { pub uid: String, } @@ -237,7 +266,7 @@ impl Service for ChanServ { Some("AOP") => xop::handle(me, from, "AOP", "op", args, ctx, db), Some("SOP") => xop::handle(me, from, "SOP", "op", args, ctx, db), Some("VOP") => xop::handle(me, from, "VOP", "voice", args, ctx, db), - Some("HELP") => ctx.notice(me, from.uid, "ChanServ registers and looks after channels. Commands: \x02REGISTER\x02, \x02INFO\x02, \x02LIST\x02, \x02SET\x02, \x02ACCESS\x02, \x02FLAGS\x02, \x02AOP\x02/\x02SOP\x02/\x02VOP\x02, \x02STATUS\x02, \x02OP\x02/\x02DEOP\x02, \x02VOICE\x02/\x02DEVOICE\x02, \x02KICK\x02, \x02BAN\x02/\x02UNBAN\x02, \x02AKICK\x02, \x02ENFORCE\x02, \x02TOPIC\x02, \x02ENTRYMSG\x02, \x02INVITE\x02, \x02GETKEY\x02, \x02SEEN\x02, \x02CLONE\x02, \x02MODE\x02, \x02MLOCK\x02, \x02DROP\x02. Operators also have \x02SUSPEND\x02/\x02UNSUSPEND\x02 and \x02NOEXPIRE\x02 <#channel> {ON|OFF}."), + 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.")), None => {} } diff --git a/modules/hostserv/src/lib.rs b/modules/hostserv/src/lib.rs index eaa9a24..5602234 100644 --- a/modules/hostserv/src/lib.rs +++ b/modules/hostserv/src/lib.rs @@ -3,7 +3,7 @@ //! with SET/DEL and review with LIST. `lib.rs` holds the dispatcher; each //! command lives in its own file. -use echo_api::{NetView, Priv, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Priv, Sender, Service, ServiceCtx, Store}; #[path = "on.rs"] mod on; @@ -32,6 +32,26 @@ mod template; #[path = "default.rs"] mod default; +const BLURB: &str = "HostServ gives you a virtual host (vhost). Activate an assigned one, pick one from the menu, or request one from an operator."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "ON", summary: "activate your vhost", detail: "Syntax: \x02ON\x02\nActivates your assigned vhost on your host." }, + HelpEntry { cmd: "OFF", summary: "restore your normal host", detail: "Syntax: \x02OFF\x02\nRestores your normal host." }, + HelpEntry { cmd: "REQUEST", summary: "request a vhost", detail: "Syntax: \x02REQUEST \x02\nAsks an operator to approve a vhost for you." }, + HelpEntry { cmd: "OFFERLIST", summary: "show the vhost menu", detail: "Syntax: \x02OFFERLIST\x02\nShows the self-serve vhost menu." }, + HelpEntry { cmd: "TAKE", summary: "take a vhost from the menu", detail: "Syntax: \x02TAKE \x02\nTakes a vhost from the menu (see OFFERLIST)." }, + HelpEntry { cmd: "SET", summary: "assign a vhost (operator)", detail: "Syntax: \x02SET [duration]\x02\nAssigns a vhost to an account. Operators only." }, + HelpEntry { cmd: "DEL", summary: "remove a vhost (operator)", detail: "Syntax: \x02DEL \x02\nRemoves an account's vhost. Operators only." }, + HelpEntry { cmd: "LIST", summary: "list vhosts (operator)", detail: "Syntax: \x02LIST\x02\nLists assigned vhosts. Operators only." }, + HelpEntry { cmd: "WAITING", summary: "pending requests (operator)", detail: "Syntax: \x02WAITING\x02\nLists pending vhost requests. Operators only." }, + HelpEntry { cmd: "ACTIVATE", summary: "approve a request (operator)", detail: "Syntax: \x02ACTIVATE \x02\nApproves a pending vhost request. Operators only." }, + HelpEntry { cmd: "REJECT", summary: "reject a request (operator)", detail: "Syntax: \x02REJECT \x02\nRejects a pending vhost request. Operators only." }, + HelpEntry { cmd: "OFFER", summary: "add a menu vhost (operator)", detail: "Syntax: \x02OFFER \x02\nAdds a vhost to the self-serve menu. Operators only." }, + HelpEntry { cmd: "OFFERDEL", summary: "remove a menu vhost (operator)", detail: "Syntax: \x02OFFERDEL \x02\nRemoves a menu entry (see OFFERLIST). Operators only." }, + HelpEntry { cmd: "FORBID", summary: "forbid a vhost pattern (operator)", detail: "Syntax: \x02FORBID \x02\nForbids vhosts matching a pattern. Operators only." }, + HelpEntry { cmd: "FORBIDDEL", summary: "remove a forbidden pattern (operator)", detail: "Syntax: \x02FORBIDDEL \x02\nRemoves a forbidden pattern (see FORBIDLIST). Operators only." }, +]; + pub struct HostServ { pub uid: String, } @@ -71,7 +91,7 @@ impl Service for HostServ { Some("FORBIDDEL") => forbid::del(me, from, args, ctx, db), Some("TEMPLATE") => template::handle(me, from, args, ctx, db), Some("DEFAULT") => default::handle(me, from, ctx, db), - Some("HELP") | None => ctx.notice(me, from.uid, "HostServ gives you a vhost: \x02ON\x02 activates your assigned vhost, \x02OFF\x02 restores your normal host, \x02REQUEST\x02 asks for one, \x02OFFERLIST\x02 + \x02TAKE\x02 pick from the menu. Operators use \x02SET\x02/\x02DEL\x02 , \x02LIST\x02, \x02WAITING\x02 + \x02ACTIVATE\x02/\x02REJECT\x02, and \x02OFFER\x02/\x02OFFERDEL\x02 for the menu."), + 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.")), } } diff --git a/modules/memoserv/src/lib.rs b/modules/memoserv/src/lib.rs index 854f580..8f189bd 100644 --- a/modules/memoserv/src/lib.rs +++ b/modules/memoserv/src/lib.rs @@ -4,7 +4,7 @@ //! account data. `lib.rs` holds the dispatcher; each command lives in its own //! file, matching NickServ/ChanServ. -use echo_api::{NetView, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "send.rs"] mod send; @@ -15,6 +15,15 @@ mod read; #[path = "del.rs"] mod del; +const BLURB: &str = "MemoServ delivers messages to registered users, online or not. You must be identified to use it."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "SEND", summary: "leave a memo for an account", detail: "Syntax: \x02SEND \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 |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 |ALL\x02\nDeletes a memo by number, or your whole mailbox." }, +]; + pub struct MemoServ { pub uid: String, } @@ -34,7 +43,7 @@ impl Service for MemoServ { let me = self.uid.as_str(); let cmd = args.first().map(|s| s.to_ascii_uppercase()); if matches!(cmd.as_deref(), Some("HELP") | None) { - ctx.notice(me, from.uid, "MemoServ delivers messages to registered users, online or not. Commands: \x02SEND\x02 , \x02LIST\x02, \x02READ\x02 |NEW|ALL, \x02DEL\x02 |ALL."); + echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()); return; } // Everything else needs you to be identified (memos are per-account). diff --git a/modules/operserv/src/lib.rs b/modules/operserv/src/lib.rs index 6b43ca1..d0ff1b7 100644 --- a/modules/operserv/src/lib.rs +++ b/modules/operserv/src/lib.rs @@ -3,7 +3,7 @@ //! announcement to every user, and KILL to disconnect one. `lib.rs` dispatches; //! each command family is its own file. -use echo_api::{NetView, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "xline.rs"] mod xline; @@ -80,13 +80,35 @@ impl Service for OperServ { Some(cmd) if cmd.eq_ignore_ascii_case("CHANKILL") => chankill::handle(me, from, args, ctx, net, db), Some(cmd) if cmd.eq_ignore_ascii_case("LOGSEARCH") => logsearch::handle(me, from, args, ctx, net), Some(cmd) if cmd.eq_ignore_ascii_case("DEFCON") => defcon::handle(me, from, args, ctx, db), - Some(cmd) if cmd.eq_ignore_ascii_case("HELP") => help(me, from, ctx), - None => help(me, from, ctx), + Some(cmd) if cmd.eq_ignore_ascii_case("HELP") => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()), + None => echo_api::help(me, from, ctx, BLURB, TOPICS, None), Some(other) => ctx.notice(me, from.uid, format!("I don't know \x02{other}\x02. Try \x02HELP\x02.")), } } } -fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx) { - ctx.notice(me, from.uid, "OperServ holds network operator tools (Priv::Admin): \x02AKILL\x02 ADD|DEL|LIST (user@host bans), \x02SQLINE\x02 ADD|DEL|LIST (nick bans), \x02SNLINE\x02 ADD|DEL|LIST (realname bans), \x02SHUN\x02 ADD|DEL|LIST (silence a host), \x02CBAN\x02 ADD|DEL|LIST (ban a channel name), \x02CHANKILL\x02 <#chan> (clear a channel), \x02GLOBAL\x02 (announce to everyone), \x02KILL\x02 [reason] (disconnect a user), \x02KICK\x02 <#chan> [reason], \x02MODE\x02 <#chan> [params], \x02IGNORE\x02 ADD|DEL|LIST (silence a user from services), \x02STATS\x02 (enforcement overview), \x02SVSNICK\x02 , \x02SVSJOIN\x02 <#chan> [key], \x02INFO\x02 ADD|DEL (staff notes — bulletins are on \x02InfoServ\x02), \x02OPER\x02 ADD|DEL|LIST (runtime operators), \x02SESSION\x02 LIST|VIEW + \x02EXCEPTION\x02 ADD|DEL|LIST (per-IP session limits), \x02JUPE\x02 | DEL | LIST, \x02LOGSEARCH\x02 [pattern] (search the action log), \x02DEFCON\x02 [1-5] (network defence level)."); -} +const BLURB: &str = "OperServ holds the network operator tools. Every command is operator-only."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "AKILL", summary: "network-ban a user@host", detail: "Syntax: \x02AKILL ADD [+expiry] | AKILL DEL | AKILL LIST [pattern]\x02\nA network-wide user@host ban." }, + HelpEntry { cmd: "SQLINE", summary: "ban a nick pattern", detail: "Syntax: \x02SQLINE ADD [+expiry] | SQLINE DEL | SQLINE LIST [pattern]\x02\nBans matching nicknames." }, + HelpEntry { cmd: "SNLINE", summary: "ban a realname pattern", detail: "Syntax: \x02SNLINE ADD [+expiry] | SNLINE DEL | SNLINE LIST [pattern]\x02\nBans matching real names." }, + HelpEntry { cmd: "SHUN", summary: "silence a host", detail: "Syntax: \x02SHUN ADD [+expiry] | SHUN DEL | SHUN LIST [pattern]\x02\nSilences a matching host: it stays connected but can't act." }, + HelpEntry { cmd: "CBAN", summary: "ban a channel name", detail: "Syntax: \x02CBAN ADD [+expiry] <#channel> | CBAN DEL | CBAN LIST [pattern]\x02\nBlocks matching channel names from being joined." }, + HelpEntry { cmd: "CHANKILL", summary: "clear a channel", detail: "Syntax: \x02CHANKILL <#channel> [reason]\x02\nRemoves everyone from a channel and holds it briefly." }, + HelpEntry { cmd: "GLOBAL", summary: "announce to everyone", detail: "Syntax: \x02GLOBAL \x02\nSends a notice to every user on the network." }, + HelpEntry { cmd: "KILL", summary: "disconnect a user", detail: "Syntax: \x02KILL [reason]\x02\nDisconnects a user from the network." }, + HelpEntry { cmd: "KICK", summary: "kick from a channel", detail: "Syntax: \x02KICK <#channel> [reason]\x02\nKicks a user from a channel." }, + HelpEntry { cmd: "MODE", summary: "set channel modes", detail: "Syntax: \x02MODE <#channel> [params]\x02\nSets modes on a channel." }, + HelpEntry { cmd: "IGNORE", summary: "silence a user from services", detail: "Syntax: \x02IGNORE ADD [+expiry] [reason] | IGNORE DEL | IGNORE LIST\x02\nMakes services ignore commands from a matching user." }, + HelpEntry { cmd: "STATS", summary: "enforcement overview", detail: "Syntax: \x02STATS\x02\nShows an overview of the enforcement lists." }, + HelpEntry { cmd: "SVSNICK", summary: "force a nick change", detail: "Syntax: \x02SVSNICK \x02\nForces a user to change nick." }, + HelpEntry { cmd: "SVSJOIN", summary: "force a channel join", detail: "Syntax: \x02SVSJOIN <#channel> [key]\x02\nForces a user to join a channel." }, + HelpEntry { cmd: "INFO", summary: "staff notes on a target", detail: "Syntax: \x02INFO | INFO ADD | INFO DEL \x02\nReads or sets staff notes on an account or channel. (Bulletins are on InfoServ.)" }, + HelpEntry { cmd: "OPER", summary: "runtime operators", detail: "Syntax: \x02OPER ADD [+duration] | OPER DEL | OPER LIST\x02\nGrants or revokes runtime operator privileges: auspex, suspend, admin." }, + HelpEntry { cmd: "SESSION", summary: "inspect per-IP sessions", detail: "Syntax: \x02SESSION LIST | SESSION VIEW \x02\nInspects per-IP session counts." }, + HelpEntry { cmd: "EXCEPTION", summary: "session-limit exceptions", detail: "Syntax: \x02EXCEPTION ADD [reason] | EXCEPTION DEL | EXCEPTION LIST\x02\nAdjusts the per-IP session limit for a mask (0 = unlimited)." }, + HelpEntry { cmd: "JUPE", summary: "jupe a server name", detail: "Syntax: \x02JUPE [reason] | JUPE DEL | JUPE LIST\x02\nBlocks a server name from linking." }, + HelpEntry { cmd: "LOGSEARCH", summary: "search the action log", detail: "Syntax: \x02LOGSEARCH [pattern]\x02\nSearches the incident and action log." }, + HelpEntry { cmd: "DEFCON", summary: "network defence level", detail: "Syntax: \x02DEFCON [1-5]\x02\nShows or sets the network defence level (5 = normal, 1 = lockdown)." }, +]; diff --git a/modules/statserv/src/lib.rs b/modules/statserv/src/lib.rs index f1350c9..39c74c7 100644 --- a/modules/statserv/src/lib.rs +++ b/modules/statserv/src/lib.rs @@ -3,13 +3,20 @@ //! (a #channel argument, for its founder). `lib.rs` holds the dispatcher; each //! view lives in its own file. -use echo_api::{NetView, Priv, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Priv, Sender, Service, ServiceCtx, Store}; #[path = "global.rs"] mod global; #[path = "channel.rs"] mod channel; +const BLURB: &str = "StatServ reports statistics: \x02SERVER\x02 for network-wide counters (operators), or a \x02#channel\x02 for its activity (founder)."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "SERVER", summary: "network-wide statistics", detail: "Syntax: \x02SERVER\x02\nShows network-wide counters. Operators only. Also \x02GLOBAL\x02." }, + HelpEntry { cmd: "CHANNEL", summary: "a channel's statistics", detail: "Syntax: \x02<#channel>\x02\nShows a channel's activity statistics. Available to the channel's founder." }, +]; + pub struct StatServ { pub uid: String, } @@ -30,17 +37,13 @@ impl Service for StatServ { match args.first().copied() { Some(chan) if chan.starts_with('#') => channel::handle(me, from, chan, ctx, net, db), Some(cmd) if cmd.eq_ignore_ascii_case("SERVER") || cmd.eq_ignore_ascii_case("GLOBAL") => global::handle(me, from, ctx, net, db), - Some(cmd) if cmd.eq_ignore_ascii_case("HELP") => help(me, from, ctx), - None => help(me, from, ctx), + Some(cmd) if cmd.eq_ignore_ascii_case("HELP") => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()), + None => echo_api::help(me, from, ctx, BLURB, TOPICS, None), Some(other) => ctx.notice(me, from.uid, format!("I don't know \x02{other}\x02. Try \x02SERVER\x02, a \x02#channel\x02, or \x02HELP\x02.")), } } } -fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx) { - ctx.notice(me, from.uid, "StatServ reports statistics: \x02SERVER\x02 for network-wide counters (operators), or \x02<#channel>\x02 for a channel's activity (its founder)."); -} - // Shared gate: the sender may see a channel's stats only as its founder or a // services admin. fn require_channel_admin(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool {