rust-irc/irc-proto/src/command.rs
Raito Bezarius 1389b438e4 feat(irc-proto/ircv3): add standard replies support
Only parsing is supported here, not reserialization.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-10-14 11:19:51 +02:00

1191 lines
46 KiB
Rust

//! Enumeration of all available client commands.
use std::str::FromStr;
use crate::chan::ChannelExt;
use crate::error::MessageParseError;
use crate::mode::{ChannelMode, Mode, UserMode};
use crate::response::Response;
use crate::standard_reply::{StandardTypes, StandardCodes};
/// List of all client commands as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812). This
/// also includes commands from the
/// [capabilities extension](https://tools.ietf.org/html/draft-mitchell-irc-capabilities-01).
/// Additionally, this includes some common additional commands from popular IRCds.
#[derive(Clone, Debug, PartialEq)]
pub enum Command {
// 3.1 Connection Registration
/// PASS :password
PASS(String),
/// NICK :nickname
NICK(String),
/// USER user mode * :realname
USER(String, String, String),
/// OPER name :password
OPER(String, String),
/// MODE nickname modes
UserMODE(String, Vec<Mode<UserMode>>),
/// SERVICE nickname reserved distribution type reserved :info
SERVICE(String, String, String, String, String, String),
/// QUIT :comment
QUIT(Option<String>),
/// SQUIT server :comment
SQUIT(String, String),
// 3.2 Channel operations
/// JOIN chanlist [chankeys] :[Real name]
JOIN(String, Option<String>, Option<String>),
/// PART chanlist :[comment]
PART(String, Option<String>),
/// MODE channel [modes [modeparams]]
ChannelMODE(String, Vec<Mode<ChannelMode>>),
/// TOPIC channel :[topic]
TOPIC(String, Option<String>),
/// NAMES [chanlist :[target]]
NAMES(Option<String>, Option<String>),
/// LIST [chanlist :[target]]
LIST(Option<String>, Option<String>),
/// INVITE nickname channel
INVITE(String, String),
/// KICK chanlist userlist :[comment]
KICK(String, String, Option<String>),
// 3.3 Sending messages
/// PRIVMSG msgtarget :message
///
/// ## Responding to a `PRIVMSG`
///
/// When responding to a message, it is not sufficient to simply copy the message target
/// (msgtarget). This will work just fine for responding to messages in channels where the
/// target is the same for all participants. However, when the message is sent directly to a
/// user, this target will be that client's username, and responding to that same target will
/// actually mean sending itself a response. In such a case, you should instead respond to the
/// user sending the message as specified in the message prefix. Since this is a common
/// pattern, there is a utility function
/// [`Message::response_target`]
/// which is used for this exact purpose.
PRIVMSG(String, String),
/// NOTICE msgtarget :message
///
/// ## Responding to a `NOTICE`
///
/// When responding to a notice, it is not sufficient to simply copy the message target
/// (msgtarget). This will work just fine for responding to messages in channels where the
/// target is the same for all participants. However, when the message is sent directly to a
/// user, this target will be that client's username, and responding to that same target will
/// actually mean sending itself a response. In such a case, you should instead respond to the
/// user sending the message as specified in the message prefix. Since this is a common
/// pattern, there is a utility function
/// [`Message::response_target`]
/// which is used for this exact purpose.
NOTICE(String, String),
// 3.4 Server queries and commands
/// MOTD :[target]
MOTD(Option<String>),
/// LUSERS [mask :[target]]
LUSERS(Option<String>, Option<String>),
/// VERSION :[target]
VERSION(Option<String>),
/// STATS [query :[target]]
STATS(Option<String>, Option<String>),
/// LINKS [[remote server] server :mask]
LINKS(Option<String>, Option<String>),
/// TIME :[target]
TIME(Option<String>),
/// CONNECT target server port :[remote server]
CONNECT(String, String, Option<String>),
/// TRACE :[target]
TRACE(Option<String>),
/// ADMIN :[target]
ADMIN(Option<String>),
/// INFO :[target]
INFO(Option<String>),
// 3.5 Service Query and Commands
/// SERVLIST [mask :[type]]
SERVLIST(Option<String>, Option<String>),
/// SQUERY servicename text
SQUERY(String, String),
// 3.6 User based queries
/// WHO [mask ["o"]]
WHO(Option<String>, Option<bool>),
/// WHOIS [target] masklist
WHOIS(Option<String>, String),
/// WHOWAS nicklist [count :[target]]
WHOWAS(String, Option<String>, Option<String>),
// 3.7 Miscellaneous messages
/// KILL nickname :comment
KILL(String, String),
/// PING server1 :[server2]
PING(String, Option<String>),
/// PONG server :[server2]
PONG(String, Option<String>),
/// ERROR :message
ERROR(String),
// 4 Optional Features
/// AWAY :[message]
AWAY(Option<String>),
/// REHASH
REHASH,
/// DIE
DIE,
/// RESTART
RESTART,
/// SUMMON user [target :[channel]]
SUMMON(String, Option<String>, Option<String>),
/// USERS :[target]
USERS(Option<String>),
/// WALLOPS :Text to be sent
WALLOPS(String),
/// USERHOST space-separated nicklist
USERHOST(Vec<String>),
/// ISON space-separated nicklist
ISON(Vec<String>),
// Non-RFC commands from InspIRCd
/// SAJOIN nickname channel
SAJOIN(String, String),
/// SAMODE target modes [modeparams]
SAMODE(String, String, Option<String>),
/// SANICK old nickname new nickname
SANICK(String, String),
/// SAPART nickname :comment
SAPART(String, String),
/// SAQUIT nickname :comment
SAQUIT(String, String),
/// NICKSERV message
NICKSERV(Vec<String>),
/// CHANSERV message
CHANSERV(String),
/// OPERSERV message
OPERSERV(String),
/// BOTSERV message
BOTSERV(String),
/// HOSTSERV message
HOSTSERV(String),
/// MEMOSERV message
MEMOSERV(String),
// IRCv3 support
/// CAP [*] COMMAND [*] :[param]
CAP(
Option<String>,
CapSubCommand,
Option<String>,
Option<String>,
),
// IRCv3.1 extensions
/// AUTHENTICATE data
AUTHENTICATE(String),
/// ACCOUNT [account name]
ACCOUNT(String),
// AWAY is already defined as a send-only message.
// AWAY(Option<String>),
// JOIN is already defined.
// JOIN(String, Option<String>, Option<String>),
// IRCv3.2 extensions
/// METADATA target COMMAND [params] :[param]
METADATA(String, Option<MetadataSubCommand>, Option<Vec<String>>),
/// MONITOR command [nicklist]
MONITOR(String, Option<String>),
/// BATCH (+/-)reference-tag [type [params]]
BATCH(String, Option<BatchSubCommand>, Option<Vec<String>>),
/// CHGHOST user host
CHGHOST(String, String),
// Default option.
/// An IRC response code with arguments and optional suffix.
Response(Response, Vec<String>),
/// https://ircv3.net/specs/extensions/standard-replies
/// [FAIL | WARN | NOTE] <command> <code> [<context>] <description>
StandardResponse(StandardTypes, String, StandardCodes, Vec<String>, String),
/// A raw IRC command unknown to the crate.
Raw(String, Vec<String>),
}
fn stringify(cmd: &str, args: &[&str]) -> String {
match args.split_last() {
Some((suffix, args)) => {
let args = args.join(" ");
let sp = if args.is_empty() { "" } else { " " };
let co = if suffix.is_empty() || suffix.contains(' ') || suffix.starts_with(':') {
":"
} else {
""
};
format!("{}{}{} {}{}", cmd, sp, args, co, suffix)
}
None => cmd.to_string(),
}
}
impl<'a> From<&'a Command> for String {
fn from(cmd: &'a Command) -> String {
match *cmd {
Command::PASS(ref p) => stringify("PASS", &[p]),
Command::NICK(ref n) => stringify("NICK", &[n]),
Command::USER(ref u, ref m, ref r) => stringify("USER", &[u, m, "*", r]),
Command::OPER(ref u, ref p) => stringify("OPER", &[u, p]),
Command::UserMODE(ref u, ref m) => format!(
"MODE {}{}",
u,
m.iter().fold(String::new(), |mut acc, mode| {
acc.push(' ');
acc.push_str(&mode.to_string());
acc
})
),
Command::SERVICE(ref nick, ref r0, ref dist, ref typ, ref r1, ref info) => {
stringify("SERVICE", &[nick, r0, dist, typ, r1, info])
}
Command::QUIT(Some(ref m)) => stringify("QUIT", &[m]),
Command::QUIT(None) => stringify("QUIT", &[]),
Command::SQUIT(ref s, ref c) => stringify("SQUIT", &[s, c]),
Command::JOIN(ref c, Some(ref k), Some(ref n)) => stringify("JOIN", &[c, k, n]),
Command::JOIN(ref c, Some(ref k), None) => stringify("JOIN", &[c, k]),
Command::JOIN(ref c, None, Some(ref n)) => stringify("JOIN", &[c, n]),
Command::JOIN(ref c, None, None) => stringify("JOIN", &[c]),
Command::PART(ref c, Some(ref m)) => stringify("PART", &[c, m]),
Command::PART(ref c, None) => stringify("PART", &[c]),
Command::ChannelMODE(ref u, ref m) => format!(
"MODE {}{}",
u,
m.iter().fold(String::new(), |mut acc, mode| {
acc.push(' ');
acc.push_str(&mode.to_string());
acc
})
),
Command::TOPIC(ref c, Some(ref t)) => stringify("TOPIC", &[c, t]),
Command::TOPIC(ref c, None) => stringify("TOPIC", &[c]),
Command::NAMES(Some(ref c), Some(ref t)) => stringify("NAMES", &[c, t]),
Command::NAMES(Some(ref c), None) => stringify("NAMES", &[c]),
Command::NAMES(None, _) => stringify("NAMES", &[]),
Command::LIST(Some(ref c), Some(ref t)) => stringify("LIST", &[c, t]),
Command::LIST(Some(ref c), None) => stringify("LIST", &[c]),
Command::LIST(None, _) => stringify("LIST", &[]),
Command::INVITE(ref n, ref c) => stringify("INVITE", &[n, c]),
Command::KICK(ref c, ref n, Some(ref r)) => stringify("KICK", &[c, n, r]),
Command::KICK(ref c, ref n, None) => stringify("KICK", &[c, n]),
Command::PRIVMSG(ref t, ref m) => stringify("PRIVMSG", &[t, m]),
Command::NOTICE(ref t, ref m) => stringify("NOTICE", &[t, m]),
Command::MOTD(Some(ref t)) => stringify("MOTD", &[t]),
Command::MOTD(None) => stringify("MOTD", &[]),
Command::LUSERS(Some(ref m), Some(ref t)) => stringify("LUSERS", &[m, t]),
Command::LUSERS(Some(ref m), None) => stringify("LUSERS", &[m]),
Command::LUSERS(None, _) => stringify("LUSERS", &[]),
Command::VERSION(Some(ref t)) => stringify("VERSION", &[t]),
Command::VERSION(None) => stringify("VERSION", &[]),
Command::STATS(Some(ref q), Some(ref t)) => stringify("STATS", &[q, t]),
Command::STATS(Some(ref q), None) => stringify("STATS", &[q]),
Command::STATS(None, _) => stringify("STATS", &[]),
Command::LINKS(Some(ref r), Some(ref s)) => stringify("LINKS", &[r, s]),
Command::LINKS(None, Some(ref s)) => stringify("LINKS", &[s]),
Command::LINKS(_, None) => stringify("LINKS", &[]),
Command::TIME(Some(ref t)) => stringify("TIME", &[t]),
Command::TIME(None) => stringify("TIME", &[]),
Command::CONNECT(ref t, ref p, Some(ref r)) => stringify("CONNECT", &[t, p, r]),
Command::CONNECT(ref t, ref p, None) => stringify("CONNECT", &[t, p]),
Command::TRACE(Some(ref t)) => stringify("TRACE", &[t]),
Command::TRACE(None) => stringify("TRACE", &[]),
Command::ADMIN(Some(ref t)) => stringify("ADMIN", &[t]),
Command::ADMIN(None) => stringify("ADMIN", &[]),
Command::INFO(Some(ref t)) => stringify("INFO", &[t]),
Command::INFO(None) => stringify("INFO", &[]),
Command::SERVLIST(Some(ref m), Some(ref t)) => stringify("SERVLIST", &[m, t]),
Command::SERVLIST(Some(ref m), None) => stringify("SERVLIST", &[m]),
Command::SERVLIST(None, _) => stringify("SERVLIST", &[]),
Command::SQUERY(ref s, ref t) => stringify("SQUERY", &[s, t]),
Command::WHO(Some(ref s), Some(true)) => stringify("WHO", &[s, "o"]),
Command::WHO(Some(ref s), _) => stringify("WHO", &[s]),
Command::WHO(None, _) => stringify("WHO", &[]),
Command::WHOIS(Some(ref t), ref m) => stringify("WHOIS", &[t, m]),
Command::WHOIS(None, ref m) => stringify("WHOIS", &[m]),
Command::WHOWAS(ref n, Some(ref c), Some(ref t)) => stringify("WHOWAS", &[n, c, t]),
Command::WHOWAS(ref n, Some(ref c), None) => stringify("WHOWAS", &[n, c]),
Command::WHOWAS(ref n, None, _) => stringify("WHOWAS", &[n]),
Command::KILL(ref n, ref c) => stringify("KILL", &[n, c]),
Command::PING(ref s, Some(ref t)) => stringify("PING", &[s, t]),
Command::PING(ref s, None) => stringify("PING", &[s]),
Command::PONG(ref s, Some(ref t)) => stringify("PONG", &[s, t]),
Command::PONG(ref s, None) => stringify("PONG", &[s]),
Command::ERROR(ref m) => stringify("ERROR", &[m]),
Command::AWAY(Some(ref m)) => stringify("AWAY", &[m]),
Command::AWAY(None) => stringify("AWAY", &[]),
Command::REHASH => stringify("REHASH", &[]),
Command::DIE => stringify("DIE", &[]),
Command::RESTART => stringify("RESTART", &[]),
Command::SUMMON(ref u, Some(ref t), Some(ref c)) => stringify("SUMMON", &[u, t, c]),
Command::SUMMON(ref u, Some(ref t), None) => stringify("SUMMON", &[u, t]),
Command::SUMMON(ref u, None, _) => stringify("SUMMON", &[u]),
Command::USERS(Some(ref t)) => stringify("USERS", &[t]),
Command::USERS(None) => stringify("USERS", &[]),
Command::WALLOPS(ref t) => stringify("WALLOPS", &[t]),
Command::USERHOST(ref u) => {
stringify("USERHOST", &u.iter().map(|s| &s[..]).collect::<Vec<_>>())
}
Command::ISON(ref u) => {
stringify("ISON", &u.iter().map(|s| &s[..]).collect::<Vec<_>>())
}
Command::SAJOIN(ref n, ref c) => stringify("SAJOIN", &[n, c]),
Command::SAMODE(ref t, ref m, Some(ref p)) => stringify("SAMODE", &[t, m, p]),
Command::SAMODE(ref t, ref m, None) => stringify("SAMODE", &[t, m]),
Command::SANICK(ref o, ref n) => stringify("SANICK", &[o, n]),
Command::SAPART(ref c, ref r) => stringify("SAPART", &[c, r]),
Command::SAQUIT(ref c, ref r) => stringify("SAQUIT", &[c, r]),
Command::NICKSERV(ref p) => {
stringify("NICKSERV", &p.iter().map(|s| &s[..]).collect::<Vec<_>>())
}
Command::CHANSERV(ref m) => stringify("CHANSERV", &[m]),
Command::OPERSERV(ref m) => stringify("OPERSERV", &[m]),
Command::BOTSERV(ref m) => stringify("BOTSERV", &[m]),
Command::HOSTSERV(ref m) => stringify("HOSTSERV", &[m]),
Command::MEMOSERV(ref m) => stringify("MEMOSERV", &[m]),
Command::CAP(None, ref s, None, Some(ref p)) => stringify("CAP", &[s.to_str(), p]),
Command::CAP(None, ref s, None, None) => stringify("CAP", &[s.to_str()]),
Command::CAP(Some(ref k), ref s, None, Some(ref p)) => {
stringify("CAP", &[k, s.to_str(), p])
}
Command::CAP(Some(ref k), ref s, None, None) => stringify("CAP", &[k, s.to_str()]),
Command::CAP(None, ref s, Some(ref c), Some(ref p)) => {
stringify("CAP", &[s.to_str(), c, p])
}
Command::CAP(None, ref s, Some(ref c), None) => stringify("CAP", &[s.to_str(), c]),
Command::CAP(Some(ref k), ref s, Some(ref c), Some(ref p)) => {
stringify("CAP", &[k, s.to_str(), c, p])
}
Command::CAP(Some(ref k), ref s, Some(ref c), None) => {
stringify("CAP", &[k, s.to_str(), c])
}
Command::AUTHENTICATE(ref d) => stringify("AUTHENTICATE", &[d]),
Command::ACCOUNT(ref a) => stringify("ACCOUNT", &[a]),
Command::METADATA(ref t, Some(ref c), None) => {
stringify("METADATA", &[&t[..], c.to_str()])
}
Command::METADATA(ref t, Some(ref c), Some(ref a)) => stringify(
"METADATA",
&vec![t, &c.to_str().to_owned()]
.iter()
.map(|s| &s[..])
.chain(a.iter().map(|s| &s[..]))
.collect::<Vec<_>>(),
),
// Note that it shouldn't be possible to have a later arg *and* be
// missing an early arg, so in order to serialize this as valid, we
// return it as just the command.
Command::METADATA(ref t, None, _) => stringify("METADATA", &[t]),
Command::MONITOR(ref c, Some(ref t)) => stringify("MONITOR", &[c, t]),
Command::MONITOR(ref c, None) => stringify("MONITOR", &[c]),
Command::BATCH(ref t, Some(ref c), Some(ref a)) => stringify(
"BATCH",
&vec![t, &c.to_str().to_owned()]
.iter()
.map(|s| &s[..])
.chain(a.iter().map(|s| &s[..]))
.collect::<Vec<_>>(),
),
Command::BATCH(ref t, Some(ref c), None) => stringify("BATCH", &[t, c.to_str()]),
Command::BATCH(ref t, None, Some(ref a)) => stringify(
"BATCH",
&vec![t]
.iter()
.map(|s| &s[..])
.chain(a.iter().map(|s| &s[..]))
.collect::<Vec<_>>(),
),
Command::BATCH(ref t, None, None) => stringify("BATCH", &[t]),
Command::CHGHOST(ref u, ref h) => stringify("CHGHOST", &[u, h]),
Command::Response(ref resp, ref a) => stringify(
&format!("{:03}", *resp as u16),
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
),
Command::StandardResponse(ref r#type, ref command, ref code, ref args, ref description) => {
match r#type {
}
},
Command::Raw(ref c, ref a) => {
stringify(c, &a.iter().map(|s| &s[..]).collect::<Vec<_>>())
}
}
}
}
impl Command {
/// Constructs a new Command.
pub fn new(cmd: &str, args: Vec<&str>) -> Result<Command, MessageParseError> {
Ok(if cmd.eq_ignore_ascii_case("PASS") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::PASS(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("NICK") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::NICK(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("USER") {
if args.len() != 4 {
raw(cmd, args)
} else {
Command::USER(args[0].to_owned(), args[1].to_owned(), args[3].to_owned())
}
} else if cmd.eq_ignore_ascii_case("OPER") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::OPER(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("MODE") {
if args.is_empty() {
raw(cmd, args)
} else if args[0].is_channel_name() {
Command::ChannelMODE(args[0].to_owned(), Mode::as_channel_modes(&args[1..])?)
} else {
Command::UserMODE(args[0].to_owned(), Mode::as_user_modes(&args[1..])?)
}
} else if cmd.eq_ignore_ascii_case("SERVICE") {
if args.len() != 6 {
raw(cmd, args)
} else {
Command::SERVICE(
args[0].to_owned(),
args[1].to_owned(),
args[2].to_owned(),
args[3].to_owned(),
args[4].to_owned(),
args[5].to_owned(),
)
}
} else if cmd.eq_ignore_ascii_case("QUIT") {
if args.is_empty() {
Command::QUIT(None)
} else if args.len() == 1 {
Command::QUIT(Some(args[0].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("SQUIT") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::SQUIT(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("JOIN") {
if args.len() == 1 {
Command::JOIN(args[0].to_owned(), None, None)
} else if args.len() == 2 {
Command::JOIN(args[0].to_owned(), Some(args[1].to_owned()), None)
} else if args.len() == 3 {
Command::JOIN(
args[0].to_owned(),
Some(args[1].to_owned()),
Some(args[2].to_owned()),
)
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("PART") {
if args.len() == 1 {
Command::PART(args[0].to_owned(), None)
} else if args.len() == 2 {
Command::PART(args[0].to_owned(), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("TOPIC") {
if args.len() == 1 {
Command::TOPIC(args[0].to_owned(), None)
} else if args.len() == 2 {
Command::TOPIC(args[0].to_owned(), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("NAMES") {
if args.is_empty() {
Command::NAMES(None, None)
} else if args.len() == 1 {
Command::NAMES(Some(args[0].to_owned()), None)
} else if args.len() == 2 {
Command::NAMES(Some(args[0].to_owned()), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("LIST") {
if args.is_empty() {
Command::LIST(None, None)
} else if args.len() == 1 {
Command::LIST(Some(args[0].to_owned()), None)
} else if args.len() == 2 {
Command::LIST(Some(args[0].to_owned()), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("INVITE") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::INVITE(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("KICK") {
if args.len() == 3 {
Command::KICK(
args[0].to_owned(),
args[1].to_owned(),
Some(args[2].to_owned()),
)
} else if args.len() == 2 {
Command::KICK(args[0].to_owned(), args[1].to_owned(), None)
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("PRIVMSG") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::PRIVMSG(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("NOTICE") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::NOTICE(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("MOTD") {
if args.is_empty() {
Command::MOTD(None)
} else if args.len() == 1 {
Command::MOTD(Some(args[0].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("LUSERS") {
if args.is_empty() {
Command::LUSERS(None, None)
} else if args.len() == 1 {
Command::LUSERS(Some(args[0].to_owned()), None)
} else if args.len() == 2 {
Command::LUSERS(Some(args[0].to_owned()), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("VERSION") {
if args.is_empty() {
Command::VERSION(None)
} else if args.len() == 1 {
Command::VERSION(Some(args[0].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("STATS") {
if args.is_empty() {
Command::STATS(None, None)
} else if args.len() == 1 {
Command::STATS(Some(args[0].to_owned()), None)
} else if args.len() == 2 {
Command::STATS(Some(args[0].to_owned()), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("LINKS") {
if args.is_empty() {
Command::LINKS(None, None)
} else if args.len() == 1 {
Command::LINKS(Some(args[0].to_owned()), None)
} else if args.len() == 2 {
Command::LINKS(Some(args[0].to_owned()), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("TIME") {
if args.is_empty() {
Command::TIME(None)
} else if args.len() == 1 {
Command::TIME(Some(args[0].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("CONNECT") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::CONNECT(args[0].to_owned(), args[1].to_owned(), None)
}
} else if cmd.eq_ignore_ascii_case("TRACE") {
if args.is_empty() {
Command::TRACE(None)
} else if args.len() == 1 {
Command::TRACE(Some(args[0].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("ADMIN") {
if args.is_empty() {
Command::ADMIN(None)
} else if args.len() == 1 {
Command::ADMIN(Some(args[0].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("INFO") {
if args.is_empty() {
Command::INFO(None)
} else if args.len() == 1 {
Command::INFO(Some(args[0].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("SERVLIST") {
if args.is_empty() {
Command::SERVLIST(None, None)
} else if args.len() == 1 {
Command::SERVLIST(Some(args[0].to_owned()), None)
} else if args.len() == 2 {
Command::SERVLIST(Some(args[0].to_owned()), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("SQUERY") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::SQUERY(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("WHO") {
if args.is_empty() {
Command::WHO(None, None)
} else if args.len() == 1 {
Command::WHO(Some(args[0].to_owned()), None)
} else if args.len() == 2 {
Command::WHO(Some(args[0].to_owned()), Some(args[1] == "o"))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("WHOIS") {
if args.len() == 1 {
Command::WHOIS(None, args[0].to_owned())
} else if args.len() == 2 {
Command::WHOIS(Some(args[0].to_owned()), args[1].to_owned())
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("WHOWAS") {
if args.len() == 1 {
Command::WHOWAS(args[0].to_owned(), None, None)
} else if args.len() == 2 {
Command::WHOWAS(args[0].to_owned(), None, Some(args[1].to_owned()))
} else if args.len() == 3 {
Command::WHOWAS(
args[0].to_owned(),
Some(args[1].to_owned()),
Some(args[2].to_owned()),
)
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("KILL") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::KILL(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("PING") {
if args.len() == 1 {
Command::PING(args[0].to_owned(), None)
} else if args.len() == 2 {
Command::PING(args[0].to_owned(), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("PONG") {
if args.len() == 1 {
Command::PONG(args[0].to_owned(), None)
} else if args.len() == 2 {
Command::PONG(args[0].to_owned(), Some(args[1].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("ERROR") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::ERROR(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("AWAY") {
if args.is_empty() {
Command::AWAY(None)
} else if args.len() == 1 {
Command::AWAY(Some(args[0].to_owned()))
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("REHASH") {
if args.is_empty() {
Command::REHASH
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("DIE") {
if args.is_empty() {
Command::DIE
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("RESTART") {
if args.is_empty() {
Command::RESTART
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("SUMMON") {
if args.len() == 1 {
Command::SUMMON(args[0].to_owned(), None, None)
} else if args.len() == 2 {
Command::SUMMON(args[0].to_owned(), Some(args[1].to_owned()), None)
} else if args.len() == 3 {
Command::SUMMON(
args[0].to_owned(),
Some(args[1].to_owned()),
Some(args[2].to_owned()),
)
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("USERS") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::USERS(Some(args[0].to_owned()))
}
} else if cmd.eq_ignore_ascii_case("WALLOPS") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::WALLOPS(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("USERHOST") {
Command::USERHOST(args.into_iter().map(|s| s.to_owned()).collect())
} else if cmd.eq_ignore_ascii_case("ISON") {
Command::USERHOST(args.into_iter().map(|s| s.to_owned()).collect())
} else if cmd.eq_ignore_ascii_case("SAJOIN") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::SAJOIN(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("SAMODE") {
if args.len() == 2 {
Command::SAMODE(args[0].to_owned(), args[1].to_owned(), None)
} else if args.len() == 3 {
Command::SAMODE(
args[0].to_owned(),
args[1].to_owned(),
Some(args[2].to_owned()),
)
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("SANICK") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::SANICK(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("SAPART") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::SAPART(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("SAQUIT") {
if args.len() != 2 {
raw(cmd, args)
} else {
Command::SAQUIT(args[0].to_owned(), args[1].to_owned())
}
} else if cmd.eq_ignore_ascii_case("NICKSERV") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::NICKSERV(args[1..].iter().map(|s| s.to_string()).collect())
}
} else if cmd.eq_ignore_ascii_case("CHANSERV") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::CHANSERV(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("OPERSERV") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::OPERSERV(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("BOTSERV") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::BOTSERV(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("HOSTSERV") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::HOSTSERV(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("MEMOSERV") {
if args.len() != 1 {
raw(cmd, args)
} else {
Command::MEMOSERV(args[0].to_owned())
}
} else if cmd.eq_ignore_ascii_case("CAP") {
if args.len() == 1 {
if let Ok(cmd) = args[0].parse() {
Command::CAP(None, cmd, None, None)
} else {
raw(cmd, args)
}
} else if args.len() == 2 {
if let Ok(cmd) = args[0].parse() {
Command::CAP(None, cmd, Some(args[1].to_owned()), None)
} else if let Ok(cmd) = args[1].parse() {
Command::CAP(Some(args[0].to_owned()), cmd, None, None)
} else {
raw(cmd, args)
}
} else if args.len() == 3 {
if let Ok(cmd) = args[0].parse() {
Command::CAP(
None,
cmd,
Some(args[1].to_owned()),
Some(args[2].to_owned()),
)
} else if let Ok(cmd) = args[1].parse() {
Command::CAP(
Some(args[0].to_owned()),
cmd,
Some(args[2].to_owned()),
None,
)
} else {
raw(cmd, args)
}
} else if args.len() == 4 {
if let Ok(cmd) = args[1].parse() {
Command::CAP(
Some(args[0].to_owned()),
cmd,
Some(args[2].to_owned()),
Some(args[3].to_owned()),
)
} else {
raw(cmd, args)
}
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("AUTHENTICATE") {
if args.len() == 1 {
Command::AUTHENTICATE(args[0].to_owned())
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("ACCOUNT") {
if args.len() == 1 {
Command::ACCOUNT(args[0].to_owned())
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("METADATA") {
match args.len() {
2 => match args[1].parse() {
Ok(c) => Command::METADATA(args[0].to_owned(), Some(c), None),
Err(_) => raw(cmd, args),
},
3.. => match args[1].parse() {
Ok(c) => Command::METADATA(
args[0].to_owned(),
Some(c),
Some(args.into_iter().skip(1).map(|s| s.to_owned()).collect()),
),
Err(_) => {
if args.len() == 3 {
Command::METADATA(
args[0].to_owned(),
None,
Some(args.into_iter().skip(1).map(|s| s.to_owned()).collect()),
)
} else {
raw(cmd, args)
}
}
},
_ => raw(cmd, args),
}
} else if cmd.eq_ignore_ascii_case("MONITOR") {
if args.len() == 2 {
Command::MONITOR(args[0].to_owned(), Some(args[1].to_owned()))
} else if args.len() == 1 {
Command::MONITOR(args[0].to_owned(), None)
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("BATCH") {
if args.len() == 1 {
Command::BATCH(args[0].to_owned(), None, None)
} else if args.len() == 2 {
Command::BATCH(args[0].to_owned(), Some(args[1].parse().unwrap()), None)
} else if args.len() > 2 {
Command::BATCH(
args[0].to_owned(),
Some(args[1].parse().unwrap()),
Some(args.iter().skip(2).map(|&s| s.to_owned()).collect()),
)
} else {
raw(cmd, args)
}
} else if cmd.eq_ignore_ascii_case("CHGHOST") {
if args.len() == 2 {
Command::CHGHOST(args[0].to_owned(), args[1].to_owned())
} else {
raw(cmd, args)
}
} else if StandardTypes::is_standard_type(cmd) {
let code = StandardCodes::from_message(args[0], args[1]);
let mut std_args: Vec<String> = args.iter().skip(2).map(|&s| s.to_owned()).collect();
let desc = std_args.pop().ok_or_else(|| MessageParseError::MissingDescriptionInStandardReply)?;
Command::StandardResponse(
StandardTypes::from_str(cmd).map_err(MessageParseError::InvalidStandardReplyType)?,
args[0].to_owned(), code, std_args, desc)
} else if let Ok(resp) = cmd.parse() {
Command::Response(resp, args.into_iter().map(|s| s.to_owned()).collect())
} else {
raw(cmd, args)
})
}
}
/// Makes a raw message from the specified command, arguments, and suffix.
fn raw(cmd: &str, args: Vec<&str>) -> Command {
Command::Raw(
cmd.to_owned(),
args.into_iter().map(|s| s.to_owned()).collect(),
)
}
/// A list of all of the subcommands for the capabilities extension.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CapSubCommand {
/// Requests a list of the server's capabilities.
LS,
/// Requests a list of the server's capabilities.
LIST,
/// Requests specific capabilities blindly.
REQ,
/// Acknowledges capabilities.
ACK,
/// Does not acknowledge certain capabilities.
NAK,
/// Ends the capability negotiation before registration.
END,
/// Signals that new capabilities are now being offered.
NEW,
/// Signasl that the specified capabilities are cancelled and no longer available.
DEL,
}
impl CapSubCommand {
/// Gets the string that corresponds to this subcommand.
pub fn to_str(&self) -> &str {
match *self {
CapSubCommand::LS => "LS",
CapSubCommand::LIST => "LIST",
CapSubCommand::REQ => "REQ",
CapSubCommand::ACK => "ACK",
CapSubCommand::NAK => "NAK",
CapSubCommand::END => "END",
CapSubCommand::NEW => "NEW",
CapSubCommand::DEL => "DEL",
}
}
}
impl FromStr for CapSubCommand {
type Err = MessageParseError;
fn from_str(s: &str) -> Result<CapSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("LS") {
Ok(CapSubCommand::LS)
} else if s.eq_ignore_ascii_case("LIST") {
Ok(CapSubCommand::LIST)
} else if s.eq_ignore_ascii_case("REQ") {
Ok(CapSubCommand::REQ)
} else if s.eq_ignore_ascii_case("ACK") {
Ok(CapSubCommand::ACK)
} else if s.eq_ignore_ascii_case("NAK") {
Ok(CapSubCommand::NAK)
} else if s.eq_ignore_ascii_case("END") {
Ok(CapSubCommand::END)
} else if s.eq_ignore_ascii_case("NEW") {
Ok(CapSubCommand::NEW)
} else if s.eq_ignore_ascii_case("DEL") {
Ok(CapSubCommand::DEL)
} else {
Err(MessageParseError::InvalidSubcommand {
cmd: "CAP",
sub: s.to_owned(),
})
}
}
}
/// A list of all the subcommands for the
/// [metadata extension](http://ircv3.net/specs/core/metadata-3.2.html).
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MetadataSubCommand {
/// Looks up the value for some keys.
GET,
/// Lists all of the metadata keys and values.
LIST,
/// Sets the value for some key.
SET,
/// Removes all metadata.
CLEAR,
}
impl MetadataSubCommand {
/// Gets the string that corresponds to this subcommand.
pub fn to_str(&self) -> &str {
match *self {
MetadataSubCommand::GET => "GET",
MetadataSubCommand::LIST => "LIST",
MetadataSubCommand::SET => "SET",
MetadataSubCommand::CLEAR => "CLEAR",
}
}
}
impl FromStr for MetadataSubCommand {
type Err = MessageParseError;
fn from_str(s: &str) -> Result<MetadataSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("GET") {
Ok(MetadataSubCommand::GET)
} else if s.eq_ignore_ascii_case("LIST") {
Ok(MetadataSubCommand::LIST)
} else if s.eq_ignore_ascii_case("SET") {
Ok(MetadataSubCommand::SET)
} else if s.eq_ignore_ascii_case("CLEAR") {
Ok(MetadataSubCommand::CLEAR)
} else {
Err(MessageParseError::InvalidSubcommand {
cmd: "METADATA",
sub: s.to_owned(),
})
}
}
}
/// [batch extension](http://ircv3.net/specs/extensions/batch-3.2.html).
#[derive(Clone, Debug, PartialEq)]
pub enum BatchSubCommand {
/// [NETSPLIT](http://ircv3.net/specs/extensions/batch/netsplit.html)
NETSPLIT,
/// [NETJOIN](http://ircv3.net/specs/extensions/batch/netsplit.html)
NETJOIN,
/// Vendor-specific BATCH subcommands.
CUSTOM(String),
}
impl BatchSubCommand {
/// Gets the string that corresponds to this subcommand.
pub fn to_str(&self) -> &str {
match *self {
BatchSubCommand::NETSPLIT => "NETSPLIT",
BatchSubCommand::NETJOIN => "NETJOIN",
BatchSubCommand::CUSTOM(ref s) => s,
}
}
}
impl FromStr for BatchSubCommand {
type Err = MessageParseError;
fn from_str(s: &str) -> Result<BatchSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("NETSPLIT") {
Ok(BatchSubCommand::NETSPLIT)
} else if s.eq_ignore_ascii_case("NETJOIN") {
Ok(BatchSubCommand::NETJOIN)
} else {
Ok(BatchSubCommand::CUSTOM(s.to_uppercase()))
}
}
}
#[cfg(test)]
mod test {
use super::Command;
use super::Response;
use crate::Message;
use crate::standard_reply::{StandardTypes, StandardCodes};
#[test]
fn format_response() {
assert!(
String::from(&Command::Response(
Response::RPL_WELCOME,
vec!["foo".into()],
)) == "001 foo"
);
}
#[test]
fn user_round_trip() {
let cmd = Command::USER("a".to_string(), "b".to_string(), "c".to_string());
let line = Message::from(cmd.clone()).to_string();
let returned_cmd = line.parse::<Message>().unwrap().command;
assert_eq!(cmd, returned_cmd);
}
#[test]
fn parse_user_message() {
let cmd = "USER a 0 * b".parse::<Message>().unwrap().command;
assert_eq!(
Command::USER("a".to_string(), "0".to_string(), "b".to_string()),
cmd
);
}
#[test]
fn parse_standard_reply() {
let msg = "FAIL BOX BOXES_INVALID STACK CLOCKWISE :Given boxes are not supported".parse::<Message>().unwrap();
assert_eq!(
msg.command,
Command::StandardResponse(StandardTypes::Fail, "BOX".to_string(), StandardCodes::Custom("BOXES_INVALID".to_string()), vec!["STACK".to_string(), "CLOCKWISE".to_string()], "Given boxes are not supported".to_string())
);
let msg = "NOTE * OPER_MESSAGE :Registering new accounts and channels has been disabled temporarily while we deal with the spam. Thanks for flying ExampleNet! -dan".parse::<Message>().unwrap();
assert_eq!(
msg.command,
Command::StandardResponse(StandardTypes::Note, "*".to_string(), StandardCodes::Custom("OPER_MESSAGE".to_string()), vec![], "Registering new accounts and channels has been disabled temporarily while we deal with the spam. Thanks for flying ExampleNet! -dan".to_string())
);
}
}