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:
parent
69a93198ff
commit
fcca8e4e3b
17 changed files with 151 additions and 6 deletions
|
|
@ -1053,6 +1053,15 @@ pub trait NetView {
|
||||||
// `limit`). An empty pattern returns the most recent; otherwise matches the
|
// `limit`). An empty pattern returns the most recent; otherwise matches the
|
||||||
// id or a case-insensitive substring of the summary.
|
// id or a case-insensitive substring of the summary.
|
||||||
fn search_incidents(&self, pattern: &str, limit: usize) -> Vec<IncidentView>;
|
fn search_incidents(&self, pattern: &str, limit: usize) -> Vec<IncidentView>;
|
||||||
|
// The network-wide help index the engine builds from every service, so
|
||||||
|
// HelpServ can front help for the whole network. `help_services` lists the
|
||||||
|
// service names; `service_help` returns one service's blurb and topics.
|
||||||
|
fn help_services(&self) -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
fn service_help(&self, _service: &str) -> Option<(&'static str, &'static [HelpEntry])> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One recorded action from the incident log: a short id (also stamped into the
|
// One recorded action from the incident log: a short id (also stamped into the
|
||||||
|
|
@ -1083,6 +1092,11 @@ pub trait Service: Send {
|
||||||
fn manages_accounts(&self) -> bool {
|
fn manages_accounts(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
// This service's HELP catalog: its blurb and per-command topics. The engine
|
||||||
|
// collects these into a network-wide index HelpServ can front. Empty default.
|
||||||
|
fn help_topics(&self) -> (&'static str, &'static [HelpEntry]) {
|
||||||
|
("", &[])
|
||||||
|
}
|
||||||
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, store: &mut dyn Store);
|
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, store: &mut dyn Store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,10 @@ impl Service for BotServ {
|
||||||
"Bot Services"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@ impl Service for ChanFix {
|
||||||
"Channel Fixer"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
// The whole service is operator-only.
|
// The whole service is operator-only.
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,10 @@ impl Service for ChanServ {
|
||||||
true
|
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) {
|
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 me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ impl Service for DiceServ {
|
||||||
"Dice Roller"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ impl Service for GroupServ {
|
||||||
"Group Service"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ mod next;
|
||||||
#[path = "close.rs"]
|
#[path = "close.rs"]
|
||||||
mod close;
|
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] = &[
|
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." },
|
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"
|
"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();
|
let me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
Some("REQUEST") | Some("HELPME") => request::handle(me, from, &args[1..], ctx, db),
|
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("TAKE") | Some("ASSIGN") => take::handle(me, from, args.get(1).copied(), ctx, db),
|
||||||
Some("NEXT") => next::handle(me, from, 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("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()),
|
// HELP [service [command]]: bare HELP is HelpServ's own; HELP <service>
|
||||||
Some(other) => ctx.notice(me, from.uid, format!("I don't know \x02{other}\x02. Try \x02REQUEST\x02 <message> or \x02HELP\x02.")),
|
// [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])),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ impl Service for HostServ {
|
||||||
"Host Services"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ impl Service for InfoServ {
|
||||||
"Information Service"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ impl Service for MemoServ {
|
||||||
"Memo Services"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
let cmd = args.first().map(|s| s.to_ascii_uppercase());
|
let cmd = args.first().map(|s| s.to_ascii_uppercase());
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,10 @@ impl Service for NickServ {
|
||||||
true
|
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) {
|
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 me = self.uid.as_str();
|
||||||
let cmd = args.first().map(|s| s.to_ascii_uppercase());
|
let cmd = args.first().map(|s| s.to_ascii_uppercase());
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@ impl Service for OperServ {
|
||||||
"Operator Service"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
// Every OperServ command is operator-only: reveal nothing to others.
|
// Every OperServ command is operator-only: reveal nothing to others.
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ impl Service for ReportServ {
|
||||||
"Report Service"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
match args.first().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ impl Service for StatServ {
|
||||||
"Statistics Service"
|
"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) {
|
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 me = self.uid.as_str();
|
||||||
match args.first().copied() {
|
match args.first().copied() {
|
||||||
|
|
|
||||||
|
|
@ -186,9 +186,19 @@ impl Engine {
|
||||||
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
||||||
let chan_service = services.iter().find(|s| s.manages_channels()).map(|s| s.uid().to_string());
|
let chan_service = services.iter().find(|s| s.manages_channels()).map(|s| s.uid().to_string());
|
||||||
let nick_service = services.iter().find(|s| s.manages_accounts()).map(|s| s.uid().to_string());
|
let nick_service = services.iter().find(|s| s.manages_accounts()).map(|s| s.uid().to_string());
|
||||||
|
// Network-wide help index: every service that publishes topics, so HelpServ
|
||||||
|
// can front help for the whole network through NetView.
|
||||||
|
let mut network = Network::default();
|
||||||
|
network.help_catalog = services
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| {
|
||||||
|
let (blurb, topics) = s.help_topics();
|
||||||
|
(!topics.is_empty()).then(|| (s.nick().to_string(), blurb, topics))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
services,
|
services,
|
||||||
network: Network::default(),
|
network,
|
||||||
db,
|
db,
|
||||||
sasl_sessions: HashMap::new(),
|
sasl_sessions: HashMap::new(),
|
||||||
reg_limiter: RegLimiter::new(),
|
reg_limiter: RegLimiter::new(),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
// The read-only network view a module sees; re-exported so the engine keeps
|
// The read-only network view a module sees; re-exported so the engine keeps
|
||||||
// naming it locally.
|
// naming it locally.
|
||||||
pub use echo_api::{IncidentView, NetView, SeenView};
|
pub use echo_api::{HelpEntry, IncidentView, NetView, SeenView};
|
||||||
|
|
||||||
// The most recent moderation/action incidents kept for LOGSEARCH.
|
// The most recent moderation/action incidents kept for LOGSEARCH.
|
||||||
const INCIDENT_CAP: usize = 10_000;
|
const INCIDENT_CAP: usize = 10_000;
|
||||||
|
|
@ -31,6 +31,9 @@ pub struct Network {
|
||||||
// ChanFix op-time scores: channel (lowercase) -> identity (account or host) ->
|
// ChanFix op-time scores: channel (lowercase) -> identity (account or host) ->
|
||||||
// score. Sampled from live op state; ephemeral (node-local, rebuilt over time).
|
// score. Sampled from live op state; ephemeral (node-local, rebuilt over time).
|
||||||
chanfix: HashMap<String, HashMap<String, u32>>,
|
chanfix: HashMap<String, HashMap<String, u32>>,
|
||||||
|
// Network-wide help index (service nick, blurb, per-command topics), built by
|
||||||
|
// the engine at startup from every service so HelpServ can front it.
|
||||||
|
pub help_catalog: Vec<(String, &'static str, &'static [HelpEntry])>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChanFix scoring caps and rates.
|
// ChanFix scoring caps and rates.
|
||||||
|
|
@ -417,4 +420,13 @@ impl NetView for Network {
|
||||||
fn op_count(&self, channel: &str) -> usize {
|
fn op_count(&self, channel: &str) -> usize {
|
||||||
Network::op_count(self, channel)
|
Network::op_count(self, channel)
|
||||||
}
|
}
|
||||||
|
fn help_services(&self) -> Vec<String> {
|
||||||
|
self.help_catalog.iter().map(|(n, _, _)| n.clone()).collect()
|
||||||
|
}
|
||||||
|
fn service_help(&self, service: &str) -> Option<(&'static str, &'static [HelpEntry])> {
|
||||||
|
self.help_catalog
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _, _)| n.eq_ignore_ascii_case(service))
|
||||||
|
.map(|(_, b, t)| (*b, *t))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3460,3 +3460,46 @@
|
||||||
let out = e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#y".into(), op: false });
|
let out = e.handle(NetEvent::Join { uid: "000AAAAAB".into(), channel: "#y".into(), op: false });
|
||||||
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#y" && modes == "+v 000AAAAAB")), "{out:?}");
|
assert!(out.iter().any(|a| matches!(a, NetAction::ChannelMode { channel, modes, .. } if channel == "#y" && modes == "+v 000AAAAAB")), "{out:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HelpServ fronts the whole network's help: it serves any service's per-command
|
||||||
|
// help, lists the services, and still answers for its own commands.
|
||||||
|
#[test]
|
||||||
|
fn helpserv_fronts_the_network_help_index() {
|
||||||
|
let path = std::env::temp_dir().join("echo-helpserv-index.jsonl");
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
let db = Db::open(&path, "test");
|
||||||
|
let mut e = Engine::new(
|
||||||
|
vec![
|
||||||
|
Box::new(NickServ { uid: "42SAAAAAA".into(), guest_nick: "Guest".into(), guest_seq: 0 }),
|
||||||
|
Box::new(echo_helpserv::HelpServ { uid: "42SAAAAAN".into() }),
|
||||||
|
],
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
e.handle(NetEvent::UserConnect { uid: "000AAAAAB".into(), nick: "foo".into(), host: "h".into(), ip: "0.0.0.0".into() });
|
||||||
|
|
||||||
|
let ask = |e: &mut Engine, text: &str| {
|
||||||
|
e.handle(NetEvent::Privmsg { from: "000AAAAAB".into(), to: "42SAAAAAN".into(), text: text.into() })
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|a| match a {
|
||||||
|
NetAction::Notice { text, .. } => Some(text),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
// HELP <service> <command> fronts that service's per-command help.
|
||||||
|
let reg = ask(&mut e, "HELP NickServ REGISTER");
|
||||||
|
assert!(reg.iter().any(|l| l.contains("Syntax:") && l.contains("REGISTER")), "{reg:?}");
|
||||||
|
|
||||||
|
// Bare HELP lists the services on the network.
|
||||||
|
let idx = ask(&mut e, "HELP");
|
||||||
|
assert!(idx.iter().any(|l| l.contains("Services:") && l.contains("NickServ")), "{idx:?}");
|
||||||
|
|
||||||
|
// A bare service name works too.
|
||||||
|
let ns = ask(&mut e, "NickServ");
|
||||||
|
assert!(ns.iter().any(|l| l.contains("IDENTIFY")), "{ns:?}");
|
||||||
|
|
||||||
|
// HelpServ's own commands still resolve.
|
||||||
|
let own = ask(&mut e, "HELP REQUEST");
|
||||||
|
assert!(own.iter().any(|l| l.contains("Syntax:") && l.contains("REQUEST")), "{own:?}");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue