From 801cf67371aeecfe477e676fe8f5b13b078adbcf Mon Sep 17 00:00:00 2001 From: catte Date: Mon, 27 May 2024 19:45:40 -0400 Subject: [PATCH 1/8] extend command_handler macro now supports aliases and restricted commands, captures docs --- doc/command-handlers.md | 22 +++++---- sable_ircd/src/command/dispatcher.rs | 27 +++++++++-- sable_ircd/src/server/mod.rs | 12 ++++- sable_macros/src/command_handler.rs | 71 ++++++++++++++++++++++++---- 4 files changed, 108 insertions(+), 24 deletions(-) diff --git a/doc/command-handlers.md b/doc/command-handlers.md index a172d691..edcd09d4 100644 --- a/doc/command-handlers.md +++ b/doc/command-handlers.md @@ -14,17 +14,23 @@ Handlers are identified by the `command_handler` attribute macro. ## The command_handler macro -The `command_handler` attribute macro has two possible forms, allowing for -multiple command dispatchers to exist. +The `command_handler` attribute macro can take several arguments: -The single-argument form - e.g. `#[command_handler("CAP")]` defines a handler -for the 'default' global dispatcher, which is used to look up client protocol -commands. +```rs +#[command_handler("PRIMARY", "ALIAS", "ALIAS2", in("PARENT"), restricted)] +``` -The two-argument form - e.g. `#[command_handler("CERT", in("NS"))]` puts the -handler into a named secondary dispatcher, in this case `"NS"`. This form is +The first argument defines the primary name of the command. Any further strings +given will be used as aliases for the command. + +If an argument is given of the form `in("PARENT")`, the command handler will be +put into a named secondary dispatcher, in this case `"PARENT"`. This form is used to define handlers for services commands, and may have other uses in the -future. +future. If this argument is not given, the handler is added to the 'default' +global dispatcher, which is used to look up client protocol commands. + +If the `restricted` keyword is added, the command will be marked as for operators +and will not be shown in `HELP` output to users. ## Async handlers diff --git a/sable_ircd/src/command/dispatcher.rs b/sable_ircd/src/command/dispatcher.rs index 67b0bc5e..6553e89a 100644 --- a/sable_ircd/src/command/dispatcher.rs +++ b/sable_ircd/src/command/dispatcher.rs @@ -1,3 +1,5 @@ +use std::collections::hash_map; + use super::{plumbing::Command, *}; /// Type alias for a boxed command context @@ -7,17 +9,21 @@ pub type BoxCommand<'cmd> = Box; /// attribute macro pub type CommandHandlerWrapper = for<'a> fn(BoxCommand<'a>) -> Option>; +#[derive(Clone)] /// A command handler registration. Constructed by the `command_handler` attribute macro. pub struct CommandRegistration { pub(super) command: &'static str, + pub(super) aliases: &'static [&'static str], pub(super) dispatcher: Option<&'static str>, pub(super) handler: CommandHandlerWrapper, + pub(super) restricted: bool, + pub(super) docs: &'static [&'static str], } /// A command dispatcher. Collects registered command handlers and allows lookup by /// command name. pub struct CommandDispatcher { - handlers: HashMap, + commands: HashMap, } inventory::collect!(CommandRegistration); @@ -42,11 +48,14 @@ impl CommandDispatcher { for reg in inventory::iter:: { if reg.dispatcher == category_name { - map.insert(reg.command.to_ascii_uppercase(), reg.handler); + map.insert(reg.command.to_ascii_uppercase(), reg.clone()); + for alias in reg.aliases { + map.insert(alias.to_ascii_uppercase(), reg.clone()); + } } } - Self { handlers: map } + Self { commands: map } } /// Look up and execute the handler function for to a given command. @@ -59,12 +68,20 @@ impl CommandDispatcher { ) -> Option> { let command: BoxCommand<'cmd> = Box::new(command); - match self.handlers.get(&command.command().to_ascii_uppercase()) { - Some(handler) => handler(command), + match self.commands.get(&command.command().to_ascii_uppercase()) { + Some(cmd) => (cmd.handler)(command), None => { command.notify_error(CommandError::CommandNotFound(command.command().to_owned())); None } } } + + pub fn get_command(&self, command: &str) -> Option<&CommandRegistration> { + self.commands.get(&command.to_ascii_uppercase()) + } + + pub fn iter_commands(&self) -> hash_map::Iter<'_, String, CommandRegistration> { + self.commands.iter() + } } diff --git a/sable_ircd/src/server/mod.rs b/sable_ircd/src/server/mod.rs index 5d48a62c..4ee6cbbc 100644 --- a/sable_ircd/src/server/mod.rs +++ b/sable_ircd/src/server/mod.rs @@ -21,7 +21,7 @@ use tokio::{ }; use std::{ - collections::VecDeque, + collections::{hash_map, VecDeque}, sync::{Arc, Weak}, time::Duration, }; @@ -116,6 +116,16 @@ impl ClientServer { self.node.name() } + /// Get a command from the server's dispatcher + pub fn get_command(&self, cmd: &str) -> Option<&CommandRegistration> { + self.command_dispatcher.get_command(cmd) + } + + /// Get a command from the server's dispatcher + pub fn iter_commands(&self) -> hash_map::Iter<'_, String, CommandRegistration> { + self.command_dispatcher.iter_commands() + } + /// Submit a command action to process in the next loop iteration. #[tracing::instrument(skip(self))] pub fn add_action(&self, act: CommandAction) { diff --git a/sable_macros/src/command_handler.rs b/sable_macros/src/command_handler.rs index 373026d9..0c014091 100644 --- a/sable_macros/src/command_handler.rs +++ b/sable_macros/src/command_handler.rs @@ -2,32 +2,67 @@ use super::*; use quote::{quote, quote_spanned}; use syn::{ - parenthesized, parse::Parse, parse_macro_input, token::In, Ident, ItemFn, LitStr, Token, + parenthesized, parse::Parse, parse_macro_input, token::In, Attribute, Ident, ItemFn, LitStr, + Meta, MetaNameValue, Token, }; struct CommandHandlerAttr { command_name: LitStr, + aliases: Vec, dispatcher: Option, + restricted: bool, } impl Parse for CommandHandlerAttr { fn parse(input: syn::parse::ParseStream) -> syn::Result { let command_name = input.parse()?; - let dispatcher = if input.parse::().is_ok() { - let content; - input.parse::()?; - let _paren = parenthesized!(content in input); - Some(content.parse()?) - } else { - None - }; + let mut aliases = vec![]; + let mut dispatcher = None; + let mut restricted = false; + while input.peek(Token![,]) { + if !input.peek2(LitStr) { + break; + } + let _ = input.parse::(); + aliases.push(input.parse()?); + } + while input.peek(Token![,]) { + let _ = input.parse::()?; + if input.peek(In) { + let content; + input.parse::()?; + let _paren = parenthesized!(content in input); + dispatcher = Some(content.parse()?); + } else if input.peek(Ident) { + if input.parse::()? == "restricted" { + restricted = true; + } + } + } Ok(Self { command_name, + aliases, dispatcher, + restricted, }) } } +pub fn command_docs(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter(|a| a.path.is_ident("doc")) + .filter_map(|a| match a.parse_meta() { + Ok(Meta::NameValue(MetaNameValue { + lit: syn::Lit::Str(s), + .. + })) => Some(s.value()), + _ => None, + }) + .map(|s| s.strip_prefix(' ').unwrap_or(&s).trim_end().to_owned()) + .collect() +} + pub fn command_handler(attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(attr as CommandHandlerAttr); let item = parse_macro_input!(item as ItemFn); @@ -43,11 +78,24 @@ pub fn command_handler(attr: TokenStream, item: TokenStream) -> TokenStream { } } + let aliases = input.aliases; + for alias in &aliases { + for c in alias.value().chars() { + if !c.is_ascii_uppercase() { + return quote_spanned!(command_name.span()=> compile_error!("Command aliases should be uppercase")).into(); + } + } + } + let dispatcher = match input.dispatcher { Some(name) => quote!( Some( #name ) ), None => quote!(None), }; + let restricted = input.restricted; + + let docs = command_docs(&item.attrs); + let body = if asyncness.is_none() { quote!( if let Err(e) = crate::command::plumbing::call_handler(ctx.as_ref(), &super::#name, ctx.args()) @@ -90,8 +138,11 @@ pub fn command_handler(attr: TokenStream, item: TokenStream) -> TokenStream { inventory::submit!(crate::command::CommandRegistration { command: #command_name, + aliases: &[ #(#aliases),* ], dispatcher: #dispatcher, - handler: call_proxy + handler: call_proxy, + restricted: #restricted, + docs: &[ #(#docs),* ], }); } ).into() From 9815d19e49321d03e6aab2a4f20c3ba7c1943473 Mon Sep 17 00:00:00 2001 From: catte Date: Mon, 27 May 2024 19:49:03 -0400 Subject: [PATCH 2/8] add HELP command --- sable_ircd/src/command/handlers/help.rs | 74 +++++++++++++++++++++++++ sable_ircd/src/command/mod.rs | 1 + sable_ircd/src/messages/numeric.rs | 6 ++ 3 files changed, 81 insertions(+) create mode 100644 sable_ircd/src/command/handlers/help.rs diff --git a/sable_ircd/src/command/handlers/help.rs b/sable_ircd/src/command/handlers/help.rs new file mode 100644 index 00000000..45d25af7 --- /dev/null +++ b/sable_ircd/src/command/handlers/help.rs @@ -0,0 +1,74 @@ +use super::*; + +use itertools::Itertools; + +#[command_handler("HELP", "UHELP")] +/// HELP [] +/// +/// HELP displays information for topic requested. +/// If no topic is requested, it will list available +/// help topics. +fn help_handler( + command: &dyn Command, + response: &dyn CommandResponse, + server: &ClientServer, + source: UserSource, + topic: Option<&str>, +) -> CommandResult { + // TODO: oper help? (and if oper help is on the same command, UHELP like solanum?) + // TODO: non-command help topics + let is_oper = command.command().to_ascii_uppercase() != "UHELP" && source.is_oper(); + + match topic { + Some(s) => { + let topic = s.to_ascii_uppercase(); + let topic = topic + .split_once(' ') + .map_or(topic.clone(), |(t, _)| t.to_string()); + + if let Some(cmd) = server.get_command(&topic) { + if cmd.docs.len() > 0 { + // TODO + if cmd.restricted && is_oper { + response.numeric(make_numeric!(HelpNotFound, &topic)); + return Ok(()); + } + let mut lines = cmd.docs.iter(); + response.numeric(make_numeric!( + HelpStart, + &topic, + lines.next().unwrap_or(&topic.as_str()) + )); + for line in lines { + response.numeric(make_numeric!(HelpText, &topic, line)); + } + response.numeric(make_numeric!(EndOfHelp, &topic)); + return Ok(()); + } + } + response.numeric(make_numeric!(HelpNotFound, &topic)); + } + None => { + let topic = "*"; + response.numeric(make_numeric!(HelpStart, topic, "Available help topics:")); + response.numeric(make_numeric!(HelpText, topic, "")); + for chunk in &server + .iter_commands() + .filter_map(|(k, v)| { + if !v.restricted || is_oper { + Some(k.to_ascii_uppercase()) + } else { + None + } + }) + .sorted() + .chunks(4) + { + let line = format!("{:16}", chunk.format(" ")); + response.numeric(make_numeric!(HelpText, topic, &line)); + } + response.numeric(make_numeric!(EndOfHelp, topic)); + } + }; + Ok(()) +} diff --git a/sable_ircd/src/command/mod.rs b/sable_ircd/src/command/mod.rs index dbd58d2e..105a0f1e 100644 --- a/sable_ircd/src/command/mod.rs +++ b/sable_ircd/src/command/mod.rs @@ -42,6 +42,7 @@ mod handlers { mod ban; mod cap; mod chathistory; + mod help; mod info; mod invite; mod join; diff --git a/sable_ircd/src/messages/numeric.rs b/sable_ircd/src/messages/numeric.rs index 712c7869..4f425b11 100644 --- a/sable_ircd/src/messages/numeric.rs +++ b/sable_ircd/src/messages/numeric.rs @@ -73,6 +73,10 @@ define_messages! { 371(Info) => { (line: &str) => ":{line}" }, 374(EndOfInfo) => { () => ":End of /INFO list" }, + 704(HelpStart) => { (subj: &str, line: &str) => "{subj} :{line}" }, + 705(HelpText) => { (subj: &str, line: &str) => "{subj} :{line}" }, + 706(EndOfHelp) => { (subj: &str) => "{subj} :End of /HELP" }, + 400(UnknownError) => { (reason: &str) => ":{reason}" }, 401(NoSuchTarget) => { (unknown: &str) => "{unknown} :No such nick/channel" }, @@ -130,6 +134,8 @@ define_messages! { 440(ServicesNotAvailable) => { () => ":Services are not available"}, + 524(HelpNotFound) => { (subj: &str) => "{subj} :No help available on this topic" }, + // https://ircv3.net/specs/extensions/monitor 730(MonOnline) => { (content: &str ) => ":{content}" }, 731(MonOffline) => { (content: &str ) => ":{content}" }, From 99a87da2dfae4d308fec56ee701a0dc68d46daa1 Mon Sep 17 00:00:00 2001 From: catte Date: Sat, 18 Apr 2026 22:26:04 -0400 Subject: [PATCH 3/8] various improvements, NS HELP support --- sable_ircd/src/command/handlers/help.rs | 145 +++++++++++++++++------- sable_ircd/src/server/mod.rs | 2 +- sable_macros/src/command_handler.rs | 4 +- 3 files changed, 106 insertions(+), 45 deletions(-) diff --git a/sable_ircd/src/command/handlers/help.rs b/sable_ircd/src/command/handlers/help.rs index 45d25af7..e2ee23e7 100644 --- a/sable_ircd/src/command/handlers/help.rs +++ b/sable_ircd/src/command/handlers/help.rs @@ -3,7 +3,7 @@ use super::*; use itertools::Itertools; #[command_handler("HELP", "UHELP")] -/// HELP [] +/// HELP \[\\] /// /// HELP displays information for topic requested. /// If no topic is requested, it will list available @@ -15,60 +15,119 @@ fn help_handler( source: UserSource, topic: Option<&str>, ) -> CommandResult { - // TODO: oper help? (and if oper help is on the same command, UHELP like solanum?) + // TODO: better restricted mechanism? // TODO: non-command help topics let is_oper = command.command().to_ascii_uppercase() != "UHELP" && source.is_oper(); match topic { - Some(s) => { - let topic = s.to_ascii_uppercase(); - let topic = topic - .split_once(' ') - .map_or(topic.clone(), |(t, _)| t.to_string()); + Some(t) => { + let topic = t.to_ascii_uppercase(); + let topic = topic.split_once(' ').map_or(topic.clone(), |(t, _)| t.to_string()); + if let Some(mut lines) = get_help(&server.command_dispatcher, &topic, is_oper) { + response.numeric(make_numeric!( + HelpStart, + &topic, + lines.next().unwrap().as_ref() + )); + for line in lines { + response.numeric(make_numeric!(HelpText, &topic, line.as_ref())); + } + response.numeric(make_numeric!(EndOfHelp, &topic)); + return Ok(()); + } else { + response.numeric(make_numeric!(HelpNotFound, &topic)); + } + } + None => { + let topic = "*"; + response.numeric(make_numeric!(HelpStart, &topic, "Available help topics:")); + response.numeric(make_numeric!(HelpText, &topic, "")); + for line in list_help(&server.command_dispatcher, is_oper) { + response.numeric(make_numeric!(HelpText, &topic, line.as_ref())); + } + response.numeric(make_numeric!(EndOfHelp, &topic)); + } + }; + Ok(()) +} - if let Some(cmd) = server.get_command(&topic) { - if cmd.docs.len() > 0 { - // TODO - if cmd.restricted && is_oper { - response.numeric(make_numeric!(HelpNotFound, &topic)); - return Ok(()); - } - let mut lines = cmd.docs.iter(); - response.numeric(make_numeric!( - HelpStart, - &topic, - lines.next().unwrap_or(&topic.as_str()) - )); - for line in lines { - response.numeric(make_numeric!(HelpText, &topic, line)); - } - response.numeric(make_numeric!(EndOfHelp, &topic)); - return Ok(()); +#[command_handler("HELP", "UHELP", in("NS"))] +/// NS HELP \[\\] +/// +/// Displays information about the topic requested. +fn ns_help_handler( + command: &dyn Command, + response: &dyn CommandResponse, + server: &ClientServer, + source: UserSource, + topic: Option<&str>, +) -> CommandResult { + let is_oper = command.command().to_ascii_uppercase() != "UHELP" && source.is_oper(); + let dispatcher = CommandDispatcher::with_category("NS"); + + match topic { + Some(t) => { + let topic = t.to_ascii_uppercase(); + let topic = topic.split_once(' ').map_or(topic.clone(), |(t, _)| t.to_string()); + if let Some(mut lines) = get_help(&dispatcher, &topic, is_oper) { + response.numeric(make_numeric!( + HelpStart, + &topic, + lines.next().unwrap().as_ref() + )); + for line in lines { + response.numeric(make_numeric!(HelpText, &topic, line.as_ref())); } + response.numeric(make_numeric!(EndOfHelp, &topic)); + return Ok(()); + } else { + response.numeric(make_numeric!(HelpNotFound, &topic)); } - response.numeric(make_numeric!(HelpNotFound, &topic)); } None => { let topic = "*"; - response.numeric(make_numeric!(HelpStart, topic, "Available help topics:")); - response.numeric(make_numeric!(HelpText, topic, "")); - for chunk in &server - .iter_commands() - .filter_map(|(k, v)| { - if !v.restricted || is_oper { - Some(k.to_ascii_uppercase()) - } else { - None - } - }) - .sorted() - .chunks(4) - { - let line = format!("{:16}", chunk.format(" ")); - response.numeric(make_numeric!(HelpText, topic, &line)); + response.numeric(make_numeric!(HelpStart, &topic, "Available help topics:")); + response.numeric(make_numeric!(HelpText, &topic, "")); + for line in list_help(&dispatcher, is_oper) { + response.numeric(make_numeric!(HelpText, &topic, line.as_ref())); } - response.numeric(make_numeric!(EndOfHelp, topic)); + response.numeric(make_numeric!(EndOfHelp, &topic)); } }; Ok(()) } + +fn get_help( + dispatcher: &CommandDispatcher, + topic: &str, + is_oper: bool, +) -> Option>> { + if let Some(cmd) = dispatcher.get_command(&topic) { + if cmd.docs.len() > 0 { + if cmd.restricted && !is_oper { + return None; + } + return Some(cmd.docs.iter()); + } + } + return None; +} + +fn list_help(dispatcher: &CommandDispatcher, is_oper: bool) -> Vec> { + let mut lines = vec![]; + for chunk in &dispatcher + .iter_commands() + .filter_map(|(k, v)| { + if (!v.restricted || is_oper) && (v.docs.len() > 0) { + Some(k.to_ascii_uppercase()) + } else { + None + } + }) + .sorted() + .chunks(4) + { + lines.push(format!("{:16}", chunk.format(" "))); + } + lines +} diff --git a/sable_ircd/src/server/mod.rs b/sable_ircd/src/server/mod.rs index 4ee6cbbc..acc4feed 100644 --- a/sable_ircd/src/server/mod.rs +++ b/sable_ircd/src/server/mod.rs @@ -74,7 +74,7 @@ pub struct ClientServer { stored_response_sinks: RwLock, action_submitter: UnboundedSender, - command_dispatcher: command::CommandDispatcher, + pub command_dispatcher: command::CommandDispatcher, connections: RwLock, /// Connections which either did not complete registration or completed it recently, diff --git a/sable_macros/src/command_handler.rs b/sable_macros/src/command_handler.rs index 0c014091..8dd90538 100644 --- a/sable_macros/src/command_handler.rs +++ b/sable_macros/src/command_handler.rs @@ -59,7 +59,9 @@ pub fn command_docs(attrs: &[Attribute]) -> Vec { })) => Some(s.value()), _ => None, }) - .map(|s| s.strip_prefix(' ').unwrap_or(&s).trim_end().to_owned()) + // XXX: markdown-stripping could be a bit more robust + .map(|s| s.strip_prefix(' ').unwrap_or(&s).trim_end() + .replace(r"\[", "[").replace(r"\]", "]").replace(r"\<", "<").replace(r"\>", ">")) .collect() } From 9aa4e87bb6563b4b252254f00f26a1db9a27edce Mon Sep 17 00:00:00 2001 From: catte Date: Sat, 18 Apr 2026 22:37:44 -0400 Subject: [PATCH 4/8] unify help command handling --- sable_ircd/src/command/handlers/help.rs | 55 +++++++++++-------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/sable_ircd/src/command/handlers/help.rs b/sable_ircd/src/command/handlers/help.rs index e2ee23e7..bbf37292 100644 --- a/sable_ircd/src/command/handlers/help.rs +++ b/sable_ircd/src/command/handlers/help.rs @@ -18,53 +18,44 @@ fn help_handler( // TODO: better restricted mechanism? // TODO: non-command help topics let is_oper = command.command().to_ascii_uppercase() != "UHELP" && source.is_oper(); - - match topic { - Some(t) => { - let topic = t.to_ascii_uppercase(); - let topic = topic.split_once(' ').map_or(topic.clone(), |(t, _)| t.to_string()); - if let Some(mut lines) = get_help(&server.command_dispatcher, &topic, is_oper) { - response.numeric(make_numeric!( - HelpStart, - &topic, - lines.next().unwrap().as_ref() - )); - for line in lines { - response.numeric(make_numeric!(HelpText, &topic, line.as_ref())); - } - response.numeric(make_numeric!(EndOfHelp, &topic)); - return Ok(()); - } else { - response.numeric(make_numeric!(HelpNotFound, &topic)); - } - } - None => { - let topic = "*"; - response.numeric(make_numeric!(HelpStart, &topic, "Available help topics:")); - response.numeric(make_numeric!(HelpText, &topic, "")); - for line in list_help(&server.command_dispatcher, is_oper) { - response.numeric(make_numeric!(HelpText, &topic, line.as_ref())); - } - response.numeric(make_numeric!(EndOfHelp, &topic)); - } - }; - Ok(()) + send_help(&server.command_dispatcher, topic, is_oper, response) } #[command_handler("HELP", "UHELP", in("NS"))] /// NS HELP \[\\] /// /// Displays information about the topic requested. +/// If no topic is requested, it will list available +/// help topics. fn ns_help_handler( command: &dyn Command, response: &dyn CommandResponse, - server: &ClientServer, source: UserSource, topic: Option<&str>, ) -> CommandResult { let is_oper = command.command().to_ascii_uppercase() != "UHELP" && source.is_oper(); let dispatcher = CommandDispatcher::with_category("NS"); + send_help(&dispatcher, topic, is_oper, response) +} + +#[command_handler("HELP", "UHELP", in("CS"))] +/// CS HELP \[\\] +/// +/// Displays information about the topic requested. +/// If no topic is requested, it will list available +/// help topics. +fn cs_help_handler( + command: &dyn Command, + response: &dyn CommandResponse, + source: UserSource, + topic: Option<&str>, +) -> CommandResult { + let is_oper = command.command().to_ascii_uppercase() != "UHELP" && source.is_oper(); + let dispatcher = CommandDispatcher::with_category("CS"); + send_help(&dispatcher, topic, is_oper, response) +} +fn send_help(dispatcher: &CommandDispatcher, topic: Option<&str>, is_oper: bool, response: &dyn CommandResponse) -> CommandResult { match topic { Some(t) => { let topic = t.to_ascii_uppercase(); From 6b87ae304530e1dc6aa89fe615a2b67164081cb9 Mon Sep 17 00:00:00 2001 From: catte Date: Sat, 18 Apr 2026 22:38:09 -0400 Subject: [PATCH 5/8] add a bunch of help texts --- sable_ircd/src/command/handlers/admin.rs | 7 +++++++ sable_ircd/src/command/handlers/away.rs | 5 +++++ sable_ircd/src/command/handlers/invite.rs | 4 ++++ sable_ircd/src/command/handlers/join.rs | 10 ++++++++++ sable_ircd/src/command/handlers/kick.rs | 6 ++++++ sable_ircd/src/command/handlers/kill.rs | 8 +++++++- sable_ircd/src/command/handlers/kline.rs | 2 +- sable_ircd/src/command/handlers/oper.rs | 3 ++- sable_ircd/src/command/handlers/services/ns/login.rs | 8 +++++--- sable_ircd/src/command/handlers/whowas.rs | 12 +++++++++++- 10 files changed, 58 insertions(+), 7 deletions(-) diff --git a/sable_ircd/src/command/handlers/admin.rs b/sable_ircd/src/command/handlers/admin.rs index f5158192..a322391d 100644 --- a/sable_ircd/src/command/handlers/admin.rs +++ b/sable_ircd/src/command/handlers/admin.rs @@ -1,6 +1,13 @@ use super::*; #[command_handler("ADMIN")] +/// ADMIN +/// +/// ADMIN shows the information that was set by the +/// administrator of the server. This information +/// can take any form that will fit in three lines +/// of text but is usually a list of contacts for +/// the persons that run the server. fn handle_admin(server: &ClientServer, response: &dyn CommandResponse) -> CommandResult { response.numeric(make_numeric!(AdminMe, server.name())); if let Some(admin_info) = &server.info_strings.admin_info { diff --git a/sable_ircd/src/command/handlers/away.rs b/sable_ircd/src/command/handlers/away.rs index 41e98a9b..a5eb2179 100644 --- a/sable_ircd/src/command/handlers/away.rs +++ b/sable_ircd/src/command/handlers/away.rs @@ -2,6 +2,11 @@ use super::*; use event::*; #[command_handler("AWAY")] +/// AWAY :[] +/// +/// With an argument, it will set you as AWAY with +/// the specified message. Without an argument, +/// it will set you back. async fn away_handler( cmd: &dyn Command, source: UserSource<'_>, diff --git a/sable_ircd/src/command/handlers/invite.rs b/sable_ircd/src/command/handlers/invite.rs index 362f923c..54f532f0 100644 --- a/sable_ircd/src/command/handlers/invite.rs +++ b/sable_ircd/src/command/handlers/invite.rs @@ -1,6 +1,10 @@ use super::*; #[command_handler("INVITE")] +/// INVITE + +/// INVITE sends a notice to the specified user that +/// you have asked them to come to the specified channel. fn handle_invite( server: &ClientServer, source: UserSource, diff --git a/sable_ircd/src/command/handlers/join.rs b/sable_ircd/src/command/handlers/join.rs index 6250eb26..8225bb94 100644 --- a/sable_ircd/src/command/handlers/join.rs +++ b/sable_ircd/src/command/handlers/join.rs @@ -1,6 +1,16 @@ use super::*; #[command_handler("JOIN")] +/// JOIN <#channel> [] +/// +/// The JOIN command allows you to enter a public chat area known as +/// a channel. Channels are prefixed with a '#'. More than one +/// channel may be specified, separated with commas (no spaces). +/// +/// If the channel has a key set, the 2nd argument must be +/// given to enter. This allows channels to be password protected. +/// +/// See also: PART, LIST async fn handle_join( server: &ClientServer, net: &Network, diff --git a/sable_ircd/src/command/handlers/kick.rs b/sable_ircd/src/command/handlers/kick.rs index 52e5f60b..7078161c 100644 --- a/sable_ircd/src/command/handlers/kick.rs +++ b/sable_ircd/src/command/handlers/kick.rs @@ -1,6 +1,12 @@ use super::*; #[command_handler("KICK")] +/// KICK :[] +/// +/// The KICK command will remove the specified user +/// from the specified channel, using the optional +/// kick message. You must be a channel operator to +/// use this command. async fn handle_kick( server: &ClientServer, cmd: &dyn Command, diff --git a/sable_ircd/src/command/handlers/kill.rs b/sable_ircd/src/command/handlers/kill.rs index 11c8dfc1..1b37ea82 100644 --- a/sable_ircd/src/command/handlers/kill.rs +++ b/sable_ircd/src/command/handlers/kill.rs @@ -1,7 +1,13 @@ use super::*; use event::*; -#[command_handler("KILL")] +#[command_handler("KILL", restricted)] +/// KILL :[] +/// +/// Disconnects the specified user from the IRC server +/// they are connected to with reason . +/// +/// (TODO) Requires Oper Priv: oper:kill fn handle_kill( server: &ClientServer, source: UserSource, diff --git a/sable_ircd/src/command/handlers/kline.rs b/sable_ircd/src/command/handlers/kline.rs index ece3ad5a..01c7b76a 100644 --- a/sable_ircd/src/command/handlers/kline.rs +++ b/sable_ircd/src/command/handlers/kline.rs @@ -4,7 +4,7 @@ use sable_network::network::ban::*; const DEFAULT_KLINE_DURATION: u32 = 1440; -#[command_handler("KLINE")] +#[command_handler("KLINE", restricted)] fn handle_kline( server: &ClientServer, response: &dyn CommandResponse, diff --git a/sable_ircd/src/command/handlers/oper.rs b/sable_ircd/src/command/handlers/oper.rs index 0660aed9..cafc378c 100644 --- a/sable_ircd/src/command/handlers/oper.rs +++ b/sable_ircd/src/command/handlers/oper.rs @@ -1,7 +1,8 @@ use super::*; use event::*; -#[command_handler("OPER")] +#[command_handler("OPER", restricted)] +/// OPER \ \ fn handle_oper( response: &dyn CommandResponse, server: &ClientServer, diff --git a/sable_ircd/src/command/handlers/services/ns/login.rs b/sable_ircd/src/command/handlers/services/ns/login.rs index 59e9af33..d5defe0e 100644 --- a/sable_ircd/src/command/handlers/services/ns/login.rs +++ b/sable_ircd/src/command/handlers/services/ns/login.rs @@ -3,9 +3,11 @@ use sable_network::rpc::{RemoteServerResponse, RemoteServicesServerResponse}; use super::*; -#[command_handler("LOGIN", in("NS"))] -#[command_handler("IDENTIFY", in("NS"))] -#[command_handler("ID", in("NS"))] +#[command_handler("LOGIN", "IDENTIFY", "ID", in("NS"))] +/// LOGIN \[\\] \ +/// +/// Log into the specified account. If \ is not specified, +/// the current nickname is used. async fn handle_login( net: &Network, source: UserSource<'_>, diff --git a/sable_ircd/src/command/handlers/whowas.rs b/sable_ircd/src/command/handlers/whowas.rs index 1cddc2b1..3d0f0947 100644 --- a/sable_ircd/src/command/handlers/whowas.rs +++ b/sable_ircd/src/command/handlers/whowas.rs @@ -4,7 +4,17 @@ const DEFAULT_COUNT: usize = 8; // Arbitrary value, that happens to match the ca // historic_nick_users #[command_handler("WHOWAS")] -/// Syntax: WHOWAS \ \[\\] +/// WHOWAS \ \[\\] +/// +/// WHOWAS will show you the last known host and whois +/// information for the specified target nick. Depending on +/// the number of times they have connected to the network, +/// there may be more than one listing for a specific user. +/// +/// If a count is specified, WHOWAS will not show more +/// than that many listings. +/// +/// The WHOWAS data will expire after some time. fn whowas_handler( network: &Network, response: &dyn CommandResponse, From 7d6a4947ff30e8080986003b9b03d8715435b8d2 Mon Sep 17 00:00:00 2001 From: catte Date: Sat, 18 Apr 2026 22:38:52 -0400 Subject: [PATCH 6/8] cargo fmt --- sable_ircd/src/command/handlers/help.rs | 11 +++++++++-- sable_macros/src/command_handler.rs | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/sable_ircd/src/command/handlers/help.rs b/sable_ircd/src/command/handlers/help.rs index bbf37292..6331c666 100644 --- a/sable_ircd/src/command/handlers/help.rs +++ b/sable_ircd/src/command/handlers/help.rs @@ -55,11 +55,18 @@ fn cs_help_handler( send_help(&dispatcher, topic, is_oper, response) } -fn send_help(dispatcher: &CommandDispatcher, topic: Option<&str>, is_oper: bool, response: &dyn CommandResponse) -> CommandResult { +fn send_help( + dispatcher: &CommandDispatcher, + topic: Option<&str>, + is_oper: bool, + response: &dyn CommandResponse, +) -> CommandResult { match topic { Some(t) => { let topic = t.to_ascii_uppercase(); - let topic = topic.split_once(' ').map_or(topic.clone(), |(t, _)| t.to_string()); + let topic = topic + .split_once(' ') + .map_or(topic.clone(), |(t, _)| t.to_string()); if let Some(mut lines) = get_help(&dispatcher, &topic, is_oper) { response.numeric(make_numeric!( HelpStart, diff --git a/sable_macros/src/command_handler.rs b/sable_macros/src/command_handler.rs index 8dd90538..81201937 100644 --- a/sable_macros/src/command_handler.rs +++ b/sable_macros/src/command_handler.rs @@ -60,8 +60,15 @@ pub fn command_docs(attrs: &[Attribute]) -> Vec { _ => None, }) // XXX: markdown-stripping could be a bit more robust - .map(|s| s.strip_prefix(' ').unwrap_or(&s).trim_end() - .replace(r"\[", "[").replace(r"\]", "]").replace(r"\<", "<").replace(r"\>", ">")) + .map(|s| { + s.strip_prefix(' ') + .unwrap_or(&s) + .trim_end() + .replace(r"\[", "[") + .replace(r"\]", "]") + .replace(r"\<", "<") + .replace(r"\>", ">") + }) .collect() } From 8fbc31034dfde1bb8da9967fc2d4ba1fbc4af8db Mon Sep 17 00:00:00 2001 From: catte Date: Sat, 18 Apr 2026 23:30:32 -0400 Subject: [PATCH 7/8] append aliases to help text --- sable_macros/src/command_handler.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/sable_macros/src/command_handler.rs b/sable_macros/src/command_handler.rs index 81201937..73116429 100644 --- a/sable_macros/src/command_handler.rs +++ b/sable_macros/src/command_handler.rs @@ -48,8 +48,8 @@ impl Parse for CommandHandlerAttr { } } -pub fn command_docs(attrs: &[Attribute]) -> Vec { - attrs +pub fn command_docs(attrs: &[Attribute], aliases: &Vec) -> Vec { + let mut lines: Vec = attrs .iter() .filter(|a| a.path.is_ident("doc")) .filter_map(|a| match a.parse_meta() { @@ -59,7 +59,7 @@ pub fn command_docs(attrs: &[Attribute]) -> Vec { })) => Some(s.value()), _ => None, }) - // XXX: markdown-stripping could be a bit more robust + // XXX: markdown-stripping could be a bit more robust/extensive .map(|s| { s.strip_prefix(' ') .unwrap_or(&s) @@ -69,7 +69,26 @@ pub fn command_docs(attrs: &[Attribute]) -> Vec { .replace(r"\<", "<") .replace(r"\>", ">") }) - .collect() + .collect(); + if aliases.len() > 0 { + let mut first = true; + let mut extra_lines = vec![String::new()]; + for chunk in aliases.chunks(6) { + let line = chunk + .into_iter() + .map(LitStr::value) + .reduce(|acc, s| format!("{acc}, {s}")) + .unwrap_or_default(); + if first { + extra_lines.push(format!("Aliases: {line}")); + first = false; + } else { + extra_lines.push(format!(" {line}")); + } + } + lines.extend(extra_lines); + } + lines } pub fn command_handler(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -103,7 +122,7 @@ pub fn command_handler(attr: TokenStream, item: TokenStream) -> TokenStream { let restricted = input.restricted; - let docs = command_docs(&item.attrs); + let docs = command_docs(&item.attrs, &aliases); let body = if asyncness.is_none() { quote!( From f693101a8d79fefdbf64143422d0ea4aaf8275c9 Mon Sep 17 00:00:00 2001 From: catte Date: Sat, 18 Apr 2026 23:33:17 -0400 Subject: [PATCH 8/8] help topics available to users/opers --- sable_ircd/src/command/handlers/help.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sable_ircd/src/command/handlers/help.rs b/sable_ircd/src/command/handlers/help.rs index 6331c666..1996455e 100644 --- a/sable_ircd/src/command/handlers/help.rs +++ b/sable_ircd/src/command/handlers/help.rs @@ -84,7 +84,19 @@ fn send_help( } None => { let topic = "*"; - response.numeric(make_numeric!(HelpStart, &topic, "Available help topics:")); + if is_oper { + response.numeric(make_numeric!( + HelpStart, + &topic, + "Help topics available to opers:" + )); + } else { + response.numeric(make_numeric!( + HelpStart, + &topic, + "Help topics available to users:" + )); + } response.numeric(make_numeric!(HelpText, &topic, "")); for line in list_help(&dispatcher, is_oper) { response.numeric(make_numeric!(HelpText, &topic, line.as_ref()));