Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions doc/command-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 22 additions & 5 deletions sable_ircd/src/command/dispatcher.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::hash_map;

use super::{plumbing::Command, *};

/// Type alias for a boxed command context
Expand All @@ -7,17 +9,21 @@ pub type BoxCommand<'cmd> = Box<dyn Command + 'cmd>;
/// attribute macro
pub type CommandHandlerWrapper = for<'a> fn(BoxCommand<'a>) -> Option<AsyncHandler<'a>>;

#[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<String, CommandHandlerWrapper>,
commands: HashMap<String, CommandRegistration>,
}

inventory::collect!(CommandRegistration);
Expand All @@ -42,11 +48,14 @@ impl CommandDispatcher {

for reg in inventory::iter::<CommandRegistration> {
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.
Expand All @@ -59,12 +68,20 @@ impl CommandDispatcher {
) -> Option<AsyncHandler<'cmd>> {
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()
}
}
7 changes: 7 additions & 0 deletions sable_ircd/src/command/handlers/admin.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions sable_ircd/src/command/handlers/away.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ use super::*;
use event::*;

#[command_handler("AWAY")]
/// AWAY :[<reason>]
///
/// 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<'_>,
Expand Down
143 changes: 143 additions & 0 deletions sable_ircd/src/command/handlers/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use super::*;

use itertools::Itertools;

#[command_handler("HELP", "UHELP")]
/// HELP \[\<topic\>\]
///
/// 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: better restricted mechanism?
// TODO: non-command help topics
let is_oper = command.command().to_ascii_uppercase() != "UHELP" && source.is_oper();
send_help(&server.command_dispatcher, topic, is_oper, response)
}

#[command_handler("HELP", "UHELP", in("NS"))]
/// NS HELP \[\<topic\>\]
///
/// 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,
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 \[\<topic\>\]
///
/// 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();
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));
}
}
None => {
let topic = "*";
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()));
}
response.numeric(make_numeric!(EndOfHelp, &topic));
}
};
Ok(())
}

fn get_help(
dispatcher: &CommandDispatcher,
topic: &str,
is_oper: bool,
) -> Option<impl Iterator<Item = impl AsRef<str>>> {
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<impl AsRef<str>> {
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
}
4 changes: 4 additions & 0 deletions sable_ircd/src/command/handlers/invite.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use super::*;

#[command_handler("INVITE")]
/// INVITE <user> <channel>

Check warning on line 4 in sable_ircd/src/command/handlers/invite.rs

View workflow job for this annotation

GitHub Actions / Linting

empty line after outer attribute

/// 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,
Expand Down
10 changes: 10 additions & 0 deletions sable_ircd/src/command/handlers/join.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
use super::*;

#[command_handler("JOIN")]
/// JOIN <#channel> [<key>]
///
/// 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,
Expand Down
6 changes: 6 additions & 0 deletions sable_ircd/src/command/handlers/kick.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use super::*;

#[command_handler("KICK")]
/// KICK <channel> <user> :[<msg>]
///
/// 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,
Expand Down
8 changes: 7 additions & 1 deletion sable_ircd/src/command/handlers/kill.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use super::*;
use event::*;

#[command_handler("KILL")]
#[command_handler("KILL", restricted)]
/// KILL <user> :[<reason>]
///
/// Disconnects the specified user from the IRC server
/// they are connected to with reason <reason>.
///
/// (TODO) Requires Oper Priv: oper:kill
fn handle_kill(
server: &ClientServer,
source: UserSource,
Expand Down
2 changes: 1 addition & 1 deletion sable_ircd/src/command/handlers/kline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion sable_ircd/src/command/handlers/oper.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use super::*;
use event::*;

#[command_handler("OPER")]
#[command_handler("OPER", restricted)]
/// OPER \<name\> \<password\>
fn handle_oper(
response: &dyn CommandResponse,
server: &ClientServer,
Expand Down
8 changes: 5 additions & 3 deletions sable_ircd/src/command/handlers/services/ns/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 \[\<account name\>\] \<password\>
///
/// Log into the specified account. If \<account name\> is not specified,
/// the current nickname is used.
async fn handle_login(
net: &Network,
source: UserSource<'_>,
Expand Down
12 changes: 11 additions & 1 deletion sable_ircd/src/command/handlers/whowas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 \<target\> \[\<count\>\]
/// WHOWAS \<target\> \[\<count\>\]
///
/// 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,
Expand Down
1 change: 1 addition & 0 deletions sable_ircd/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod handlers {
mod ban;
mod cap;
mod chathistory;
mod help;
mod info;
mod invite;
mod join;
Expand Down
Loading
Loading