diff --git a/api/src/lib.rs b/api/src/lib.rs index dbd12bc..971309d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1130,6 +1130,37 @@ fn glob_match(pattern: &str, text: &str) -> bool { pi == p.len() } +// One command's help: its name, a one-line summary shown in the command list, +// and the detail shown for `HELP ` (newline-separated lines). +pub struct HelpEntry { + pub cmd: &'static str, + pub summary: &'static str, + pub detail: &'static str, +} + +// Centralized HELP for a service: `HELP` lists the commands, `HELP ` +// prints that command's detail. Every service routes its HELP command here, so +// the shape is identical across every service on the network. +pub fn help(me: &str, from: &Sender, ctx: &mut ServiceCtx, blurb: &str, topics: &[HelpEntry], topic: Option<&str>) { + match topic { + None => { + ctx.notice(me, from.uid, blurb); + for e in topics { + ctx.notice(me, from.uid, format!(" \x02{}\x02 {}", e.cmd, e.summary)); + } + 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(e) => { + for line in e.detail.lines() { + ctx.notice(me, from.uid, line); + } + } + None => ctx.notice(me, from.uid, format!("No help for \x02{}\x02. Type \x02HELP\x02 for the command list.", t.to_ascii_uppercase())), + }, + } +} + // Format a Unix timestamp (seconds) as "YYYY-MM-DD HH:MM:SS UTC", using Howard // Hinnant's civil-from-days algorithm so no date crate is needed. pub fn human_time(ts: u64) -> String { diff --git a/modules/chanfix/src/lib.rs b/modules/chanfix/src/lib.rs index 776d677..cc87f3e 100644 --- a/modules/chanfix/src/lib.rs +++ b/modules/chanfix/src/lib.rs @@ -6,13 +6,28 @@ //! SCORES <#channel> shows the standings; CHANFIX <#channel> performs the fix. //! `lib.rs` holds the dispatcher; each command lives in its own file. -use echo_api::{NetView, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "scores.rs"] mod scores; #[path = "fix.rs"] mod fix; +const BLURB: &str = "ChanFix recovers opless channels from accumulated op-time. It defers to ChanServ and won't touch a registered channel. Operator-only."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { + cmd: "SCORES", + summary: "show a channel's op-time standings", + detail: "Syntax: \x02SCORES <#channel>\x02\nShows the accumulated op-time scores, highest first. These are the identities ChanFix would reop.", + }, + HelpEntry { + cmd: "CHANFIX", + summary: "reop a channel's trusted regulars", + detail: "Syntax: \x02CHANFIX <#channel>\x02\nReops the present regulars of an opless channel by their accumulated op-time. Needs the channel opless (under three ops) with real history, and refuses a ChanServ-registered channel.", + }, +]; + pub struct ChanFix { pub uid: String, } @@ -38,7 +53,7 @@ impl Service for ChanFix { match args.first().map(|s| s.to_ascii_uppercase()).as_deref() { Some("SCORES") => scores::handle(me, from, args.get(1).copied(), ctx, net), Some("CHANFIX") | Some("FIX") => fix::handle(me, from, args.get(1).copied(), ctx, net, db), - Some("HELP") | None => ctx.notice(me, from.uid, "ChanFix recovers opless channels from accumulated op-time. \x02SCORES\x02 <#channel> shows the standings; \x02CHANFIX\x02 <#channel> reops its trusted regulars. It won't touch a ChanServ-registered channel."), + 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 \x02SCORES\x02 or \x02CHANFIX\x02 <#channel>.")), } } diff --git a/modules/diceserv/src/lib.rs b/modules/diceserv/src/lib.rs index 18bb19a..3c88792 100644 --- a/modules/diceserv/src/lib.rs +++ b/modules/diceserv/src/lib.rs @@ -6,13 +6,22 @@ //! `lib.rs` holds the dispatcher; the command lives in roll.rs and the //! expression evaluator in expr.rs. -use echo_api::{NetView, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "expr.rs"] mod expr; #[path = "roll.rs"] mod roll; +const BLURB: &str = "DiceServ rolls dice and evaluates maths for tabletop games. Dice: \x022d6\x02, \x02d20\x02, \x02d%\x02. Also + - * / ^ %, parentheses, functions (sqrt, floor, min, ...), \x02pi\x02/\x02e\x02, and \x023~2d6\x02 to repeat."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "ROLL", summary: "roll dice, whole-number result", detail: "Syntax: \x02ROLL \x02\nEvaluates a dice/math expression and rounds to a whole number, e.g. ROLL 2d6+3." }, + HelpEntry { cmd: "CALC", summary: "evaluate, keeping decimals", detail: "Syntax: \x02CALC \x02\nEvaluates an expression and keeps the decimals." }, + HelpEntry { cmd: "EXROLL", summary: "ROLL and show each die", detail: "Syntax: \x02EXROLL \x02\nLike ROLL, and also shows each individual die rolled." }, + HelpEntry { cmd: "EXCALC", summary: "CALC and show each die", detail: "Syntax: \x02EXCALC \x02\nLike CALC, and also shows each individual die rolled." }, +]; + pub struct DiceServ { pub uid: String, } @@ -35,7 +44,7 @@ impl Service for DiceServ { Some("EXROLL") => roll::handle(me, from, &args[1..], ctx, true, true), Some("CALC") => roll::handle(me, from, &args[1..], ctx, false, false), Some("EXCALC") => roll::handle(me, from, &args[1..], ctx, true, false), - Some("HELP") | None => ctx.notice(me, from.uid, "DiceServ rolls dice and does maths. \x02ROLL\x02 (whole-number result), \x02CALC\x02 (keeps decimals), \x02EXROLL\x02/\x02EXCALC\x02 also show each die. Dice: \x022d6\x02, \x02d20\x02, \x02d%\x02. Also + - * / ^ %, parentheses, functions (sqrt, floor, min, …), \x02pi\x02/\x02e\x02, and \x023~2d6\x02 to repeat."), + 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 \x02ROLL\x02, \x02CALC\x02, or \x02HELP\x02.")), } } diff --git a/modules/groupserv/src/lib.rs b/modules/groupserv/src/lib.rs index 1cfc7d0..4ed2acb 100644 --- a/modules/groupserv/src/lib.rs +++ b/modules/groupserv/src/lib.rs @@ -8,7 +8,7 @@ //! `lib.rs` holds the dispatcher and the two shared guards; each command lives //! in its own file. -use echo_api::{NetView, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "register.rs"] mod register; @@ -25,6 +25,18 @@ mod del; #[path = "flags.rs"] mod flags; +const BLURB: &str = "GroupServ manages user groups: a \x02!name\x02 owning member accounts. A channel can grant access to a group, and every member then inherits it."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "REGISTER", summary: "create a group you found", detail: "Syntax: \x02REGISTER \x02\nCreates a group with you as founder. A group name starts with '!', e.g. !staff." }, + HelpEntry { cmd: "DROP", summary: "delete a group you founded", detail: "Syntax: \x02DROP \x02\nDeletes a group. Founder only." }, + HelpEntry { cmd: "INFO", summary: "show a group's founder and size", detail: "Syntax: \x02INFO \x02\nShows a group's founder and member count." }, + HelpEntry { cmd: "LIST", summary: "list groups you belong to", detail: "Syntax: \x02LIST\x02\nLists the groups you belong to. Operators see every group." }, + HelpEntry { cmd: "ADD", summary: "add a member", detail: "Syntax: \x02ADD \x02\nAdds a plain member. Needs the founder or the 'f' flag." }, + HelpEntry { cmd: "DEL", summary: "remove a member", detail: "Syntax: \x02DEL \x02\nRemoves a member. Needs the founder or the 'f' flag." }, + HelpEntry { cmd: "FLAGS", summary: "view or change group flags", detail: "Syntax: \x02FLAGS [account [+/-flags]]\x02\nLists, shows, or changes group-access flags (F founder, f manage, i invite, c channel-access, s set, m memo). Changing needs the founder or the 'f' flag." }, +]; + pub struct GroupServ { pub uid: String, } @@ -50,7 +62,7 @@ impl Service for GroupServ { Some("ADD") => add::handle(me, from, args.get(1).copied(), args.get(2).copied(), ctx, db), Some("DEL") => del::handle(me, from, args.get(1).copied(), args.get(2).copied(), ctx, db), Some("FLAGS") => flags::handle(me, from, args.get(1).copied(), args.get(2).copied(), args.get(3).copied(), ctx, db), - Some("HELP") | None => ctx.notice(me, from.uid, "GroupServ manages user groups. \x02REGISTER\x02 , \x02DROP\x02 , \x02INFO\x02 , \x02LIST\x02, \x02ADD\x02/\x02DEL\x02 , \x02FLAGS\x02 [account [+/-flags]]. Grant a group channel access with ChanServ \x02FLAGS #chan !group +o\x02 — every member then inherits it."), + 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 \x02HELP\x02.")), } } diff --git a/modules/helpserv/src/lib.rs b/modules/helpserv/src/lib.rs index 8f8c09b..76b7c39 100644 --- a/modules/helpserv/src/lib.rs +++ b/modules/helpserv/src/lib.rs @@ -7,7 +7,7 @@ //! `lib.rs` holds the dispatcher and the shared guard/claim helpers; each //! command lives in its own file. -use echo_api::{NetView, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "request.rs"] mod request; @@ -24,6 +24,18 @@ 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 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, } @@ -49,7 +61,7 @@ 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 => ctx.notice(me, from.uid, "HelpServ is the help desk. \x02REQUEST\x02 opens a ticket for the staff; \x02CANCEL\x02 withdraws yours. Operators: \x02LIST\x02 [ALL], \x02VIEW\x02 , \x02TAKE\x02 , \x02NEXT\x02, \x02CLOSE\x02 ."), + 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 or \x02HELP\x02.")), } } diff --git a/modules/infoserv/src/lib.rs b/modules/infoserv/src/lib.rs index 79328ce..d253a8a 100644 --- a/modules/infoserv/src/lib.rs +++ b/modules/infoserv/src/lib.rs @@ -9,7 +9,7 @@ //! and any operator may OLIST. `lib.rs` holds the dispatcher; each command //! (parameterised by bulletin kind) lives in its own file. -use echo_api::{NetView, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "post.rs"] mod post; @@ -22,6 +22,17 @@ mod list; const PUBLIC: &str = "logon"; const OPER: &str = "oper"; +const BLURB: &str = "InfoServ holds the network's information bulletins: public ones show on connect, oper ones on login."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "POST", summary: "post a public bulletin", detail: "Syntax: \x02POST \x02\nAdds a public bulletin, shown to everyone on connect. Admin only. Also \x02ADD\x02." }, + HelpEntry { cmd: "LIST", summary: "show public bulletins", detail: "Syntax: \x02LIST\x02\nShows the public bulletins. Open to everyone." }, + HelpEntry { cmd: "DEL", summary: "remove a public bulletin", detail: "Syntax: \x02DEL \x02\nRemoves a public bulletin by its listed number. Admin only. Also \x02REMOVE\x02." }, + HelpEntry { cmd: "OPOST", summary: "post an oper bulletin", detail: "Syntax: \x02OPOST \x02\nAdds an oper bulletin, shown to operators on login. Admin only." }, + HelpEntry { cmd: "OLIST", summary: "show oper bulletins", detail: "Syntax: \x02OLIST\x02\nShows the oper bulletins. Operators only." }, + HelpEntry { cmd: "ODEL", summary: "remove an oper bulletin", detail: "Syntax: \x02ODEL \x02\nRemoves an oper bulletin by its listed number. Admin only." }, +]; + pub struct InfoServ { pub uid: String, } @@ -48,7 +59,7 @@ impl Service for InfoServ { Some("LIST") | None => list::handle(me, from, PUBLIC, false, ctx, db), // Oper bulletins are for operators only. Some("OLIST") => list::handle(me, from, OPER, true, ctx, db), - Some("HELP") => ctx.notice(me, from.uid, "InfoServ holds the network's information bulletins. \x02LIST\x02 shows the public ones. Operators: \x02POST\x02 / \x02DEL\x02 (public, shown on connect), \x02OPOST\x02 / \x02OLIST\x02 / \x02ODEL\x02 (oper-only, shown on login)."), + 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 \x02{other}\x02. Try \x02LIST\x02 or \x02HELP\x02.")), } } diff --git a/modules/nickserv/src/lib.rs b/modules/nickserv/src/lib.rs index 104bd48..35b7bcb 100644 --- a/modules/nickserv/src/lib.rs +++ b/modules/nickserv/src/lib.rs @@ -1,5 +1,5 @@ use echo_api::Store; -use echo_api::{Sender, Service, ServiceCtx}; +use echo_api::{HelpEntry, Sender, Service, ServiceCtx}; use echo_api::NetView; #[path = "register.rs"] @@ -38,6 +38,29 @@ mod noexpire; #[path = "password.rs"] mod password; +const BLURB: &str = "NickServ looks after your nickname and account: register it, identify to it, and manage its settings."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "REGISTER", summary: "register your nick as an account", detail: "Syntax: \x02REGISTER [email]\x02\nRegisters your current nick as an account. If an email is given and confirmation is on, you get a code to \x02CONFIRM\x02." }, + HelpEntry { cmd: "IDENTIFY", summary: "log in to your account", detail: "Syntax: \x02IDENTIFY [account] \x02\nLogs you in. Also \x02ID\x02." }, + HelpEntry { cmd: "LOGOUT", summary: "log out to a guest nick", detail: "Syntax: \x02LOGOUT\x02\nLogs you out and moves you to a guest nick. Also \x02LOGOFF\x02." }, + HelpEntry { cmd: "INFO", summary: "show account information", detail: "Syntax: \x02INFO [account]\x02\nShows account information. The email is shown only to the owner." }, + HelpEntry { cmd: "ALIST", summary: "list channels you have access on", detail: "Syntax: \x02ALIST\x02\nLists the channels you hold access on." }, + HelpEntry { cmd: "SET", summary: "change password or email", detail: "Syntax: \x02SET PASSWORD \x02 or \x02SET EMAIL
\x02\nChanges your account password or email." }, + HelpEntry { cmd: "GROUP", summary: "link this nick to an account", detail: "Syntax: \x02GROUP \x02\nLinks your current nick to an account as an alias, so identifying under it logs into that account." }, + HelpEntry { cmd: "GLIST", summary: "list your grouped nicks", detail: "Syntax: \x02GLIST\x02\nLists the nicks grouped to your account." }, + HelpEntry { cmd: "UNGROUP", summary: "remove a grouped nick", detail: "Syntax: \x02UNGROUP [nick]\x02\nRemoves a grouped nick (your current one by default)." }, + HelpEntry { cmd: "GHOST", summary: "disconnect a session on your nick", detail: "Syntax: \x02GHOST [password]\x02\nDisconnects a session using a nick you own. Also \x02RECOVER\x02." }, + HelpEntry { cmd: "RESETPASS", summary: "reset your password by email", detail: "Syntax: \x02RESETPASS \x02, then \x02RESETPASS \x02\nEmails a reset code, then sets a new password with it." }, + HelpEntry { cmd: "CONFIRM", summary: "confirm your email", detail: "Syntax: \x02CONFIRM \x02\nConfirms the email on a newly registered account with the code you were sent." }, + HelpEntry { cmd: "DROP", summary: "delete your account", detail: "Syntax: \x02DROP \x02\nDeletes your account and releases the channels you founded." }, + HelpEntry { cmd: "CERT", summary: "manage SASL EXTERNAL certs", detail: "Syntax: \x02CERT ADD [fingerprint]\x02, \x02CERT DEL \x02, \x02CERT LIST\x02\nManages the TLS certificate fingerprints that can log in via SASL EXTERNAL." }, + HelpEntry { cmd: "AJOIN", summary: "auto-join channels on login", detail: "Syntax: \x02AJOIN ADD <#channel>\x02, \x02AJOIN DEL <#channel>\x02, \x02AJOIN LIST\x02\nChannels you are auto-joined to when you identify." }, + HelpEntry { cmd: "SUSPEND", summary: "block an account (operator)", detail: "Syntax: \x02SUSPEND [reason]\x02\nBlocks an account from logging in. Operators only." }, + HelpEntry { cmd: "UNSUSPEND", summary: "lift a suspension (operator)", detail: "Syntax: \x02UNSUSPEND \x02\nLifts a suspension. Operators only." }, + HelpEntry { cmd: "NOEXPIRE", summary: "pin against expiry (operator)", detail: "Syntax: \x02NOEXPIRE {ON|OFF}\x02\nPins an account so inactivity expiry never drops it. Operators only." }, +]; + pub struct NickServ { pub uid: String, // Nick prefix assigned on LOGOUT (default "Guest"); a per-session sequence is @@ -88,7 +111,7 @@ impl Service for NickServ { Some("SUSPEND") => suspend::handle(me, from, args, ctx, net, db, true), Some("UNSUSPEND") => suspend::handle(me, from, args, ctx, net, db, false), Some("NOEXPIRE") => noexpire::handle(me, from, args, ctx, db), - Some("HELP") => ctx.notice(me, from.uid, "NickServ looks after your nickname. Commands: \x02REGISTER\x02 [email], \x02IDENTIFY\x02 [account] , \x02INFO\x02 [account], \x02ALIST\x02, \x02GROUP\x02/\x02GLIST\x02/\x02UNGROUP\x02, \x02GHOST\x02 [password], \x02SET\x02 PASSWORD|EMAIL, \x02AJOIN\x02 ADD|DEL|LIST, \x02RESETPASS\x02 , \x02CONFIRM\x02 , \x02DROP\x02 , \x02LOGOUT\x02, \x02CERT\x02 ADD|DEL|LIST [fingerprint]. Operators also have \x02SUSPEND\x02/\x02UNSUSPEND\x02 and \x02NOEXPIRE\x02 {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/reportserv/src/lib.rs b/modules/reportserv/src/lib.rs index 270441f..b32746c 100644 --- a/modules/reportserv/src/lib.rs +++ b/modules/reportserv/src/lib.rs @@ -8,7 +8,7 @@ //! `lib.rs` holds the dispatcher and the shared oper guard; each command lives //! in its own file. -use echo_api::{NetView, Sender, Service, ServiceCtx, Store}; +use echo_api::{HelpEntry, NetView, Sender, Service, ServiceCtx, Store}; #[path = "report.rs"] mod report; @@ -21,6 +21,16 @@ mod close; #[path = "del.rs"] mod del; +const BLURB: &str = "ReportServ takes abuse reports. Anyone can \x02REPORT\x02; operators review the queue."; + +const TOPICS: &[HelpEntry] = &[ + HelpEntry { cmd: "REPORT", summary: "file an abuse report", detail: "Syntax: \x02REPORT \x02\nTells the staff about a problem. Rate-limited." }, + HelpEntry { cmd: "LIST", summary: "list reports", detail: "Syntax: \x02LIST [ALL]\x02\nOperators: list open reports, or every report with ALL." }, + HelpEntry { cmd: "VIEW", summary: "read a report", detail: "Syntax: \x02VIEW \x02\nOperators: read a report in full. Also \x02READ\x02." }, + HelpEntry { cmd: "CLOSE", summary: "resolve a report", detail: "Syntax: \x02CLOSE \x02\nOperators: mark a report resolved. Also \x02RESOLVE\x02." }, + HelpEntry { cmd: "DEL", summary: "delete a report", detail: "Syntax: \x02DEL \x02\nOperators: delete a report outright. Also \x02REMOVE\x02." }, +]; + pub struct ReportServ { pub uid: String, } @@ -44,7 +54,7 @@ impl Service for ReportServ { Some("VIEW") | Some("READ") => view::handle(me, from, args.get(1).copied(), ctx, db), Some("CLOSE") | Some("RESOLVE") => close::handle(me, from, args.get(1).copied(), ctx, db), Some("DEL") | Some("REMOVE") => del::handle(me, from, args.get(1).copied(), ctx, db), - Some("HELP") | None => ctx.notice(me, from.uid, "ReportServ takes abuse reports. \x02REPORT\x02 tells the staff about a problem. Operators review with \x02LIST\x02 [ALL], \x02VIEW\x02 , \x02CLOSE\x02 , \x02DEL\x02 ."), + 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 \x02REPORT\x02 or \x02HELP\x02.")), } }