//! HelpServ is a help desk. A user opens a ticket with REQUEST (or HELPME); it //! joins a queue that — being event-sourced and surfaced by the engine's audit //! feed — is announced to staff and shows up in OperServ LOGSEARCH, the same //! trail as reports. Operators work the queue: LIST, VIEW, TAKE (claim), NEXT //! (claim the oldest), and CLOSE. A user can CANCEL their own open ticket. //! //! `lib.rs` holds the dispatcher and the shared guard/claim helpers; each //! command lives in its own file. use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "request.rs"] mod request; #[path = "cancel.rs"] mod cancel; #[path = "list.rs"] mod list; #[path = "view.rs"] mod view; #[path = "take.rs"] mod take; #[path = "next.rs"] mod next; #[path = "close.rs"] mod close; #[path = "extbans.rs"] mod extbans; const BLURB: &str = "HelpServ is the help desk and the network's help index. Open a ticket with \x02REQUEST\x02; operators work the queue."; const TOPICS: &[HelpEntry] = &[ HelpEntry { cmd: "REQUEST", summary: "open a help-desk ticket", detail: "Syntax: \x02REQUEST \x02\nOpens a ticket for the staff. Also \x02HELPME\x02." }, HelpEntry { cmd: "CANCEL", summary: "withdraw your ticket", detail: "Syntax: \x02CANCEL\x02\nWithdraws your own newest open ticket." }, HelpEntry { cmd: "LIST", summary: "list the ticket queue", detail: "Syntax: \x02LIST [ALL]\x02\nOperators: list open tickets, or every ticket with ALL." }, HelpEntry { cmd: "VIEW", summary: "read a ticket", detail: "Syntax: \x02VIEW \x02\nOperators: read a ticket in full. Also \x02READ\x02." }, HelpEntry { cmd: "TAKE", summary: "claim a ticket", detail: "Syntax: \x02TAKE \x02\nOperators: claim a specific ticket. Also \x02ASSIGN\x02." }, HelpEntry { cmd: "NEXT", summary: "claim the oldest ticket", detail: "Syntax: \x02NEXT\x02\nOperators: claim the oldest unassigned ticket." }, HelpEntry { cmd: "CLOSE", summary: "resolve a ticket", detail: "Syntax: \x02CLOSE \x02\nOperators: resolve a ticket. Also \x02RESOLVE\x02." }, ]; pub struct HelpServ { pub uid: String, } impl Service for HelpServ { fn nick(&self) -> &str { "HelpServ" } fn uid(&self) -> &str { &self.uid } fn gecos(&self) -> &str { "Help Service" } fn help_topics(&self) -> (&'static str, &'static [HelpEntry]) { (BLURB, TOPICS) } fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, db: &mut dyn Store) { let me = self.uid.as_str(); match args.first().map(|s| s.to_ascii_uppercase()).as_deref() { Some("REQUEST") | Some("HELPME") => request::handle(me, from, &args[1..], ctx, net, db), Some("CANCEL") => cancel::handle(me, from, ctx, db), Some("LIST") => list::handle(me, from, args.get(1).copied(), ctx, db), Some("VIEW") | Some("READ") => view::handle(me, from, args.get(1).copied(), ctx, db), Some("TAKE") | Some("ASSIGN") => take::handle(me, from, args.get(1).copied(), ctx, db), Some("NEXT") => next::handle(me, from, ctx, db), Some("CLOSE") | Some("RESOLVE") => close::handle(me, from, args.get(1).copied(), ctx, db), // The extended-ban reference, listing this network's live extban set. Some("EXTBANS") | Some("EXTBAN") => extbans::handle(me, from, ctx, db), // HELP [service [command]]: bare HELP is HelpServ's own; HELP // [command] fronts any other service's help through the shared renderer. Some("HELP") | None => match args.get(1).copied() { Some(t) if t.eq_ignore_ascii_case("extbans") || t.eq_ignore_ascii_case("extban") => extbans::handle(me, from, ctx, db), other => match other.and_then(|s| net.service_help(s)) { Some((blurb, topics)) => echo_api::help(me, from, ctx, blurb, topics, args.get(2).copied()), None => { echo_api::help(me, from, ctx, BLURB, TOPICS, other); if other.is_none() { ctx.notice(me, from.uid, format!("For a service's own commands, type \x02HELP \x02. Services: {}.", net.help_services().join(", "))); ctx.notice(me, from.uid, "For the extended ban types you can use, type \x02HELP EXTBANS\x02."); } } }, }, // Not a HelpServ command — maybe the name of a service to get help for. Some(_) => match net.service_help(args[0]) { Some((blurb, topics)) => echo_api::help(me, from, ctx, blurb, topics, args.get(1).copied()), None => ctx.notice(me, from.uid, format!("I don't know \x02{}\x02. Try \x02REQUEST\x02 , \x02HELP\x02, or a service name (e.g. \x02NickServ\x02).", args[0])), }, } } } // Working the help queue is operator-only. fn require_oper(me: &str, from: &Sender, ctx: &mut ServiceCtx) -> bool { echo_api::require_oper(me, from, ctx, None, "working the help queue") } // Claim ticket `id` for the calling operator (shared by TAKE and NEXT). fn take_id(me: &str, from: &Sender, id: u64, ctx: &mut ServiceCtx, db: &mut dyn Store) { let handler = from.account.unwrap_or(from.nick); if db.help_take(id, handler) { let msg = db.help_ticket(id).map(|t| t.message).unwrap_or_default(); ctx.notice(me, from.uid, format!("You took ticket \x02#{id}\x02: {msg}")); } else { ctx.notice(me, from.uid, format!("Ticket \x02#{id}\x02 isn't open (or doesn't exist).")); } }