//! 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>), /// SERVICE nickname reserved distribution type reserved :info SERVICE(String, String, String, String, String, String), /// QUIT :comment QUIT(Option), /// SQUIT server :comment SQUIT(String, String), // 3.2 Channel operations /// JOIN chanlist [chankeys] :[Real name] JOIN(String, Option, Option), /// PART chanlist :[comment] PART(String, Option), /// MODE channel [modes [modeparams]] ChannelMODE(String, Vec>), /// TOPIC channel :[topic] TOPIC(String, Option), /// NAMES [chanlist :[target]] NAMES(Option, Option), /// LIST [chanlist :[target]] LIST(Option, Option), /// INVITE nickname channel INVITE(String, String), /// KICK chanlist userlist :[comment] KICK(String, String, Option), // 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), /// LUSERS [mask :[target]] LUSERS(Option, Option), /// VERSION :[target] VERSION(Option), /// STATS [query :[target]] STATS(Option, Option), /// LINKS [[remote server] server :mask] LINKS(Option, Option), /// TIME :[target] TIME(Option), /// CONNECT target server port :[remote server] CONNECT(String, String, Option), /// TRACE :[target] TRACE(Option), /// ADMIN :[target] ADMIN(Option), /// INFO :[target] INFO(Option), // 3.5 Service Query and Commands /// SERVLIST [mask :[type]] SERVLIST(Option, Option), /// SQUERY servicename text SQUERY(String, String), // 3.6 User based queries /// WHO [mask ["o"]] WHO(Option, Option), /// WHOIS [target] masklist WHOIS(Option, String), /// WHOWAS nicklist [count :[target]] WHOWAS(String, Option, Option), // 3.7 Miscellaneous messages /// KILL nickname :comment KILL(String, String), /// PING server1 :[server2] PING(String, Option), /// PONG server :[server2] PONG(String, Option), /// ERROR :message ERROR(String), // 4 Optional Features /// AWAY :[message] AWAY(Option), /// REHASH REHASH, /// DIE DIE, /// RESTART RESTART, /// SUMMON user [target :[channel]] SUMMON(String, Option, Option), /// USERS :[target] USERS(Option), /// WALLOPS :Text to be sent WALLOPS(String), /// USERHOST space-separated nicklist USERHOST(Vec), /// ISON space-separated nicklist ISON(Vec), // Non-RFC commands from InspIRCd /// SAJOIN nickname channel SAJOIN(String, String), /// SAMODE target modes [modeparams] SAMODE(String, String, Option), /// SANICK old nickname new nickname SANICK(String, String), /// SAPART nickname :comment SAPART(String, String), /// SAQUIT nickname :comment SAQUIT(String, String), /// NICKSERV message NICKSERV(Vec), /// 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, CapSubCommand, Option, Option, ), // IRCv3.1 extensions /// AUTHENTICATE data AUTHENTICATE(String), /// ACCOUNT [account name] ACCOUNT(String), // AWAY is already defined as a send-only message. // AWAY(Option), // JOIN is already defined. // JOIN(String, Option, Option), // IRCv3.2 extensions /// METADATA target COMMAND [params] :[param] METADATA(String, Option, Option>), /// MONITOR command [nicklist] MONITOR(String, Option), /// BATCH (+/-)reference-tag [type [params]] BATCH(String, Option, Option>), /// CHGHOST user host CHGHOST(String, String), // Default option. /// An IRC response code with arguments and optional suffix. Response(Response, Vec), /// https://ircv3.net/specs/extensions/standard-replies /// [FAIL | WARN | NOTE] [] StandardResponse(StandardTypes, String, StandardCodes, Vec, String), /// A raw IRC command unknown to the crate. Raw(String, Vec), } 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::>()) } Command::ISON(ref u) => { stringify("ISON", &u.iter().map(|s| &s[..]).collect::>()) } 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::>()) } 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::>(), ), // 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::>(), ), 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::>(), ), 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::>(), ), 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::>()) } } } } impl Command { /// Constructs a new Command. pub fn new(cmd: &str, args: Vec<&str>) -> Result { 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 = 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 { 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 { 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 { 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::().unwrap().command; assert_eq!(cmd, returned_cmd); } #[test] fn parse_user_message() { let cmd = "USER a 0 * b".parse::().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::().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::().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()) ); } }