Front the whole network's help through HelpServ

Each service now exposes its help catalog via Service::help_topics; the
engine collects them into a network-wide index on Network, reachable
through two new NetView methods. HelpServ uses it: HELP <service>
[command] (or a bare service name) serves any service's per-command
help, bare HELP lists the services, and its own ticket commands still
work. No cross-crate dependency: the catalog flows through the SDK's
NetView, keeping every module dependent only on echo-api.
This commit is contained in:
Jean Chevronnet 2026-07-14 18:37:45 +00:00
parent 69a93198ff
commit fcca8e4e3b
No known key found for this signature in database
17 changed files with 151 additions and 6 deletions

View file

@ -68,6 +68,10 @@ impl Service for BotServ {
"Bot Services"
}
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() {

View file

@ -43,6 +43,10 @@ impl Service for ChanFix {
"Channel Fixer"
}
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();
// The whole service is operator-only.

View file

@ -90,6 +90,10 @@ impl Service for ChanServ {
true
}
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() {

View file

@ -37,6 +37,10 @@ impl Service for DiceServ {
"Dice Roller"
}
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() {

View file

@ -52,6 +52,10 @@ impl Service for GroupServ {
"Group 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() {

View file

@ -24,7 +24,7 @@ mod next;
#[path = "close.rs"]
mod close;
const BLURB: &str = "HelpServ is the help desk. Open a ticket with \x02REQUEST\x02; operators work the queue.";
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 <message>\x02\nOpens a ticket for the staff. Also \x02HELPME\x02." },
@ -51,7 +51,11 @@ impl Service for HelpServ {
"Help Service"
}
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, _net: &dyn NetView, db: &mut dyn Store) {
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, db),
@ -61,8 +65,22 @@ impl Service for HelpServ {
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),
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 \x02{other}\x02. Try \x02REQUEST\x02 <message> or \x02HELP\x02.")),
// HELP [service [command]]: bare HELP is HelpServ's own; HELP <service>
// [command] fronts any other service's help through the shared renderer.
Some("HELP") | None => match args.get(1).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, args.get(1).copied());
if args.get(1).is_none() {
ctx.notice(me, from.uid, format!("For a service's own commands, type \x02HELP <service>\x02. Services: {}.", net.help_services().join(", ")));
}
}
},
// 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 <message>, \x02HELP\x02, or a service name (e.g. \x02NickServ\x02).", args[0])),
},
}
}
}

View file

@ -67,6 +67,10 @@ impl Service for HostServ {
"Host Services"
}
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() {

View file

@ -48,6 +48,10 @@ impl Service for InfoServ {
"Information 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() {

View file

@ -39,6 +39,10 @@ impl Service for MemoServ {
"Memo Services"
}
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();
let cmd = args.first().map(|s| s.to_ascii_uppercase());

View file

@ -83,6 +83,10 @@ impl Service for NickServ {
true
}
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();
let cmd = args.first().map(|s| s.to_ascii_uppercase());

View file

@ -51,6 +51,10 @@ impl Service for OperServ {
"Operator 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();
// Every OperServ command is operator-only: reveal nothing to others.

View file

@ -46,6 +46,10 @@ impl Service for ReportServ {
"Report 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() {

View file

@ -32,6 +32,10 @@ impl Service for StatServ {
"Statistics 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().copied() {