irc-proto-1.0.0/.cargo_vcs_info.json0000644000000001470000000000100127520ustar { "git": { "sha1": "b45c5fa88daa21faac7ce00db2cf78d9d228f734" }, "path_in_vcs": "irc-proto" }irc-proto-1.0.0/Cargo.toml0000644000000023300000000000100107440ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" rust-version = "1.60" name = "irc-proto" version = "1.0.0" authors = ["Aaron Weiss "] description = "The IRC protocol distilled." documentation = "https://docs.rs/irc-proto/" keywords = [ "irc", "protocol", "tokio", ] categories = ["network-programming"] license = "MPL-2.0" repository = "https://github.com/aatxe/irc" [dependencies.bytes] version = "1.4.0" optional = true [dependencies.encoding] version = "0.2.33" [dependencies.thiserror] version = "1.0.40" [dependencies.tokio] version = "1.27.0" optional = true [dependencies.tokio-util] version = "0.7.7" features = ["codec"] optional = true [features] default = [ "bytes", "tokio", "tokio-util", ] [badges.travis-ci] repository = "aatxe/irc" irc-proto-1.0.0/Cargo.toml.orig000064400000000000000000000012641046102023000144320ustar 00000000000000[package] name = "irc-proto" version = "1.0.0" authors = ["Aaron Weiss "] edition = "2018" rust-version = "1.60" description = "The IRC protocol distilled." documentation = "https://docs.rs/irc-proto/" repository = "https://github.com/aatxe/irc" license = "MPL-2.0" keywords = ["irc", "protocol", "tokio"] categories = ["network-programming"] [badges] travis-ci = { repository = "aatxe/irc" } [features] default = ["bytes", "tokio", "tokio-util"] [dependencies] encoding = "0.2.33" thiserror = "1.0.40" bytes = { version = "1.4.0", optional = true } tokio = { version = "1.27.0", optional = true } tokio-util = { version = "0.7.7", features = ["codec"], optional = true } irc-proto-1.0.0/src/caps.rs000064400000000000000000000077231046102023000136340ustar 00000000000000//! Enumeration of all supported IRCv3 capability extensions. /// List of all supported IRCv3 capability extensions from the /// [IRCv3 specifications](http://ircv3.net/irc/). #[derive(Debug, PartialEq)] pub enum Capability { /// [multi-prefix](http://ircv3.net/specs/extensions/multi-prefix-3.1.html) MultiPrefix, /// [sasl](http://ircv3.net/specs/extensions/sasl-3.1.html) Sasl, /// [account-notify](http://ircv3.net/specs/extensions/account-notify-3.1.html) AccountNotify, /// [away-notify](http://ircv3.net/specs/extensions/away-notify-3.1.html) AwayNotify, /// [extended-join](http://ircv3.net/specs/extensions/extended-join-3.1.html) ExtendedJoin, /// [metadata](http://ircv3.net/specs/core/metadata-3.2.html) Metadata, /// [metadata-notify](http://ircv3.net/specs/core/metadata-3.2.html) MetadataNotify, /// [monitor](http://ircv3.net/specs/core/monitor-3.2.html) Monitor, /// [account-tag](http://ircv3.net/specs/extensions/account-tag-3.2.html) AccountTag, /// [batch](http://ircv3.net/specs/extensions/batch-3.2.html) Batch, /// [cap-notify](http://ircv3.net/specs/extensions/cap-notify-3.2.html) CapNotify, /// [chghost](http://ircv3.net/specs/extensions/chghost-3.2.html) ChgHost, /// [echo-message](http://ircv3.net/specs/extensions/echo-message-3.2.html) EchoMessage, /// [invite-notify](http://ircv3.net/specs/extensions/invite-notify-3.2.html) InviteNotify, /// [server-time](http://ircv3.net/specs/extensions/server-time-3.2.html) ServerTime, /// [userhost-in-names](http://ircv3.net/specs/extensions/userhost-in-names-3.2.html) UserhostInNames, /// Custom IRCv3 capability extensions Custom(&'static str), } /// List of IRCv3 capability negotiation versions. pub enum NegotiationVersion { /// [IRCv3.1](http://ircv3.net/specs/core/capability-negotiation-3.1.html) V301, /// [IRCv3.2](http://ircv3.net/specs/core/capability-negotiation-3.2.html) V302, } impl AsRef for Capability { fn as_ref(&self) -> &str { match *self { Capability::MultiPrefix => "multi-prefix", Capability::Sasl => "sasl", Capability::AccountNotify => "account-notify", Capability::AwayNotify => "away-notify", Capability::ExtendedJoin => "extended-join", Capability::Metadata => "metadata", Capability::MetadataNotify => "metadata-notify", Capability::Monitor => "monitor", Capability::AccountTag => "account-tag", Capability::Batch => "batch", Capability::CapNotify => "cap-notify", Capability::ChgHost => "chghost", Capability::EchoMessage => "echo-message", Capability::InviteNotify => "invite-notify", Capability::ServerTime => "server-time", Capability::UserhostInNames => "userhost-in-names", Capability::Custom(s) => s, } } } #[cfg(test)] mod test { use super::Capability::*; #[test] fn to_str() { assert_eq!(MultiPrefix.as_ref(), "multi-prefix"); assert_eq!(Sasl.as_ref(), "sasl"); assert_eq!(AccountNotify.as_ref(), "account-notify"); assert_eq!(AwayNotify.as_ref(), "away-notify"); assert_eq!(ExtendedJoin.as_ref(), "extended-join"); assert_eq!(Metadata.as_ref(), "metadata"); assert_eq!(MetadataNotify.as_ref(), "metadata-notify"); assert_eq!(Monitor.as_ref(), "monitor"); assert_eq!(AccountTag.as_ref(), "account-tag"); assert_eq!(Batch.as_ref(), "batch"); assert_eq!(CapNotify.as_ref(), "cap-notify"); assert_eq!(ChgHost.as_ref(), "chghost"); assert_eq!(EchoMessage.as_ref(), "echo-message"); assert_eq!(InviteNotify.as_ref(), "invite-notify"); assert_eq!(ServerTime.as_ref(), "server-time"); assert_eq!(UserhostInNames.as_ref(), "userhost-in-names"); assert_eq!(Custom("example").as_ref(), "example"); } } irc-proto-1.0.0/src/chan.rs000064400000000000000000000011751046102023000136120ustar 00000000000000//! An extension trait that provides the ability to check if a string is a channel name. /// An extension trait giving strings a function to check if they are a channel. pub trait ChannelExt { /// Returns true if the specified name is a channel name. fn is_channel_name(&self) -> bool; } impl<'a> ChannelExt for &'a str { fn is_channel_name(&self) -> bool { self.starts_with('#') || self.starts_with('&') || self.starts_with('+') || self.starts_with('!') } } impl ChannelExt for String { fn is_channel_name(&self) -> bool { (&self[..]).is_channel_name() } } irc-proto-1.0.0/src/colors.rs000064400000000000000000000137141046102023000142040ustar 00000000000000//! An extension trait that provides the ability to strip IRC colors from a string use std::borrow::Cow; enum ParserState { Text, ColorCode, Foreground1(char), Foreground2, Comma, Background1(char), } struct Parser { state: ParserState, } /// An extension trait giving strings a function to strip IRC colors pub trait FormattedStringExt<'a> { /// Returns true if the string contains color, bold, underline or italics fn is_formatted(&self) -> bool; /// Returns the string with all color, bold, underline and italics stripped fn strip_formatting(self) -> Cow<'a, str>; } const FORMAT_CHARACTERS: &[char] = &[ '\x02', // bold '\x1F', // underline '\x16', // reverse '\x0F', // normal '\x03', // color ]; impl<'a> FormattedStringExt<'a> for &'a str { fn is_formatted(&self) -> bool { self.contains(FORMAT_CHARACTERS) } fn strip_formatting(self) -> Cow<'a, str> { if !self.is_formatted() { return Cow::Borrowed(self); } let mut s = String::from(self); strip_formatting(&mut s); Cow::Owned(s) } } fn strip_formatting(buf: &mut String) { let mut parser = Parser::new(); buf.retain(|cur| parser.next(cur)); } impl Parser { fn new() -> Self { Parser { state: ParserState::Text, } } fn next(&mut self, cur: char) -> bool { use self::ParserState::*; match self.state { Text | Foreground1(_) | Foreground2 if cur == '\x03' => { self.state = ColorCode; false } Text => !FORMAT_CHARACTERS.contains(&cur), ColorCode if cur.is_ascii_digit() => { self.state = Foreground1(cur); false } Foreground1('0') if cur.is_ascii_digit() => { // can consume another digit if previous char was 0. self.state = Foreground2; false } Foreground1('1') if cur.is_digit(6) => { // can consume another digit if previous char was 1. self.state = Foreground2; false } Foreground1(_) if cur.is_digit(6) => { self.state = Text; true } Foreground1(_) if cur == ',' => { self.state = Comma; false } Foreground2 if cur == ',' => { self.state = Comma; false } Comma if (cur.is_ascii_digit()) => { self.state = Background1(cur); false } Background1(prev) if cur.is_digit(6) => { // can only consume another digit if previous char was 1. self.state = Text; prev != '1' } _ => { self.state = Text; !FORMAT_CHARACTERS.contains(&cur) } } } } impl FormattedStringExt<'static> for String { fn is_formatted(&self) -> bool { self.as_str().is_formatted() } fn strip_formatting(mut self) -> Cow<'static, str> { if !self.is_formatted() { return Cow::Owned(self); } strip_formatting(&mut self); Cow::Owned(self) } } #[cfg(test)] mod test { use crate::colors::FormattedStringExt; use std::borrow::Cow; macro_rules! test_formatted_string_ext { { $( $name:ident ( $($line:tt)* ), )* } => { $( mod $name { use super::*; test_formatted_string_ext!(@ $($line)*); } )* }; (@ $text:expr, should stripped into $expected:expr) => { #[test] fn test_formatted() { assert!($text.is_formatted()); } #[test] fn test_strip() { assert_eq!($text.strip_formatting(), $expected); } }; (@ $text:expr, is not formatted) => { #[test] fn test_formatted() { assert!(!$text.is_formatted()); } #[test] fn test_strip() { assert_eq!($text.strip_formatting(), $text); } } } test_formatted_string_ext! { blank("", is not formatted), blank2(" ", is not formatted), blank3("\t\r\n", is not formatted), bold("l\x02ol", should stripped into "lol"), bold_from_string(String::from("l\x02ol"), should stripped into "lol"), bold_hangul("우왕\x02굳", should stripped into "우왕굳"), fg_color("l\x033ol", should stripped into "lol"), fg_color2("l\x0312ol", should stripped into "lol"), fg_color3("l\x0302ol", should stripped into "lol"), fg_color4("l\x030ol", should stripped into "lol"), fg_color5("l\x0309ol", should stripped into "lol"), fg_bg_11("l\x031,2ol", should stripped into "lol"), fg_bg_21("l\x0312,3ol", should stripped into "lol"), fg_bg_12("l\x031,12ol", should stripped into "lol"), fg_bg_22("l\x0312,13ol", should stripped into "lol"), fg_bold("l\x0309\x02ol", should stripped into "lol"), string_with_multiple_colors("hoo\x034r\x033a\x0312y", should stripped into "hooray"), string_with_digit_after_color("\x0344\x0355\x0366", should stripped into "456"), string_with_multiple_2digit_colors("hoo\x0310r\x0311a\x0312y", should stripped into "hooray"), string_with_digit_after_2digit_color("\x031212\x031111\x031010", should stripped into "121110"), thinking("🤔...", is not formatted), unformatted("a plain text", is not formatted), } #[test] fn test_strip_no_allocation_for_unformatted_text() { if let Cow::Borrowed(formatted) = "plain text".strip_formatting() { assert_eq!(formatted, "plain text"); } else { panic!("allocation detected"); } } } irc-proto-1.0.0/src/command.rs000064400000000000000000001305471046102023000143250ustar 00000000000000//! 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; /// 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), /// 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::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 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; #[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 ); } } irc-proto-1.0.0/src/error.rs000064400000000000000000000037661046102023000140420ustar 00000000000000//! IRC protocol errors using `failure`. use thiserror::Error; /// A `Result` type for IRC `ProtocolErrors`. pub type Result = ::std::result::Result; /// An IRC protocol error. #[derive(Debug, Error)] pub enum ProtocolError { /// An internal I/O error. #[error("an io error occurred")] Io(#[source] std::io::Error), /// Error for invalid messages. #[error("invalid message: {}", string)] InvalidMessage { /// The string that failed to parse. string: String, /// The detailed message parsing error. #[source] cause: MessageParseError, }, } impl From for ProtocolError { fn from(e: std::io::Error) -> ProtocolError { ProtocolError::Io(e) } } /// Errors that occur when parsing messages. #[derive(Debug, Error)] pub enum MessageParseError { /// The message was empty. #[error("empty message")] EmptyMessage, /// The command was invalid (i.e. missing). #[error("invalid command")] InvalidCommand, /// The mode string was malformed. #[error("invalid mode string: {}", string)] InvalidModeString { /// The invalid mode string. string: String, /// The detailed mode parsing error. #[source] cause: ModeParseError, }, /// The subcommand used was invalid. #[error("invalid {} subcommand: {}", cmd, sub)] InvalidSubcommand { /// The command whose invalid subcommand was referenced. cmd: &'static str, /// The invalid subcommand. sub: String, }, } /// Errors that occur while parsing mode strings. #[derive(Debug, Error)] pub enum ModeParseError { /// Invalid modifier used in a mode string (only + and - are valid). #[error("invalid mode modifier: {}", modifier)] InvalidModeModifier { /// The invalid mode modifier. modifier: char, }, /// Missing modifier used in a mode string. #[error("missing mode modifier")] MissingModeModifier, } irc-proto-1.0.0/src/irc.rs000064400000000000000000000032711046102023000134550ustar 00000000000000//! Implementation of IRC codec for Tokio. use bytes::BytesMut; use tokio_util::codec::{Decoder, Encoder}; use crate::error; use crate::line::LineCodec; use crate::message::Message; /// An IRC codec built around an inner codec. pub struct IrcCodec { inner: LineCodec, } impl IrcCodec { /// Creates a new instance of IrcCodec wrapping a LineCodec with the specific encoding. pub fn new(label: &str) -> error::Result { LineCodec::new(label).map(|codec| IrcCodec { inner: codec }) } /// Sanitizes the input string by cutting up to (and including) the first occurence of a line /// terminiating phrase (`\r\n`, `\r`, or `\n`). This is used in sending messages through the /// codec to prevent the injection of additional commands. pub fn sanitize(mut data: String) -> String { // n.b. ordering matters here to prefer "\r\n" over "\r" if let Some((pos, len)) = ["\r\n", "\r", "\n"] .iter() .flat_map(|needle| data.find(needle).map(|pos| (pos, needle.len()))) .min_by_key(|&(pos, _)| pos) { data.truncate(pos + len); } data } } impl Decoder for IrcCodec { type Item = Message; type Error = error::ProtocolError; fn decode(&mut self, src: &mut BytesMut) -> error::Result> { self.inner .decode(src) .and_then(|res| res.map_or(Ok(None), |msg| msg.parse::().map(Some))) } } impl Encoder for IrcCodec { type Error = error::ProtocolError; fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> { self.inner.encode(IrcCodec::sanitize(msg.to_string()), dst) } } irc-proto-1.0.0/src/lib.rs000064400000000000000000000012601046102023000134420ustar 00000000000000//! Support for the IRC protocol using Tokio. #![warn(missing_docs)] pub mod caps; pub mod chan; pub mod colors; pub mod command; pub mod error; #[cfg(feature = "tokio")] pub mod irc; #[cfg(feature = "tokio")] pub mod line; pub mod message; pub mod mode; pub mod prefix; pub mod response; pub use self::caps::{Capability, NegotiationVersion}; pub use self::chan::ChannelExt; pub use self::colors::FormattedStringExt; pub use self::command::{BatchSubCommand, CapSubCommand, Command}; #[cfg(feature = "tokio")] pub use self::irc::IrcCodec; pub use self::message::Message; pub use self::mode::{ChannelMode, Mode, UserMode}; pub use self::prefix::Prefix; pub use self::response::Response; irc-proto-1.0.0/src/line.rs000064400000000000000000000054201046102023000136250ustar 00000000000000//! Implementation of line-delimiting codec for Tokio. use std::io; use bytes::BytesMut; use encoding::label::encoding_from_whatwg_label; use encoding::{DecoderTrap, EncoderTrap, EncodingRef}; use tokio_util::codec::{Decoder, Encoder}; use crate::error; /// A line-based codec parameterized by an encoding. pub struct LineCodec { encoding: EncodingRef, next_index: usize, } impl LineCodec { /// Creates a new instance of LineCodec from the specified encoding. pub fn new(label: &str) -> error::Result { encoding_from_whatwg_label(label) .map(|enc| LineCodec { encoding: enc, next_index: 0, }) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, &format!("Attempted to use unknown codec {}.", label)[..], ) .into() }) } } impl Decoder for LineCodec { type Item = String; type Error = error::ProtocolError; fn decode(&mut self, src: &mut BytesMut) -> error::Result> { if let Some(offset) = src[self.next_index..].iter().position(|b| *b == b'\n') { // Remove the next frame from the buffer. let line = src.split_to(self.next_index + offset + 1); // Set the search start index back to 0 since we found a newline. self.next_index = 0; // Decode the line using the codec's encoding. match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) { Ok(data) => Ok(Some(data)), Err(data) => Err(io::Error::new( io::ErrorKind::InvalidInput, &format!("Failed to decode {} as {}.", data, self.encoding.name())[..], ) .into()), } } else { // Set the search start index to the current length since we know that none of the // characters we've already looked at are newlines. self.next_index = src.len(); Ok(None) } } } impl Encoder for LineCodec { type Error = error::ProtocolError; fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> { // Encode the message using the codec's encoding. let data: error::Result> = self .encoding .encode(&msg, EncoderTrap::Replace) .map_err(|data| { io::Error::new( io::ErrorKind::InvalidInput, &format!("Failed to encode {} as {}.", data, self.encoding.name())[..], ) .into() }); // Write the encoded message to the output buffer. dst.extend(&data?); Ok(()) } } irc-proto-1.0.0/src/message.rs000064400000000000000000000433561046102023000143340ustar 00000000000000//! A module providing a data structure for messages to and from IRC servers. use std::borrow::ToOwned; use std::fmt::{Display, Formatter, Result as FmtResult, Write}; use std::str::FromStr; use crate::chan::ChannelExt; use crate::command::Command; use crate::error; use crate::error::{MessageParseError, ProtocolError}; use crate::prefix::Prefix; /// A data structure representing an IRC message according to the protocol specification. It /// consists of a collection of IRCv3 tags, a prefix (describing the source of the message), and /// the protocol command. If the command is unknown, it is treated as a special raw command that /// consists of a collection of arguments and the special suffix argument. Otherwise, the command /// is parsed into a more useful form as described in [`Command`]. #[derive(Clone, PartialEq, Debug)] pub struct Message { /// Message tags as defined by [IRCv3.2](http://ircv3.net/specs/core/message-tags-3.2.html). /// These tags are used to add extended information to the given message, and are commonly used /// in IRCv3 extensions to the IRC protocol. pub tags: Option>, /// The message prefix (or source) as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812). pub prefix: Option, /// The IRC command, parsed according to the known specifications. The command itself and its /// arguments (including the special suffix argument) are captured in this component. pub command: Command, } impl Message { /// Creates a new message from the given components. /// /// # Example /// ``` /// # extern crate irc_proto; /// # use irc_proto::Message; /// # fn main() { /// let message = Message::new( /// Some("nickname!username@hostname"), "JOIN", vec!["#channel"] /// ).unwrap(); /// # } /// ``` pub fn new( prefix: Option<&str>, command: &str, args: Vec<&str>, ) -> Result { Message::with_tags(None, prefix, command, args) } /// Creates a new IRCv3.2 message from the given components, including message tags. These tags /// are used to add extended information to the given message, and are commonly used in IRCv3 /// extensions to the IRC protocol. pub fn with_tags( tags: Option>, prefix: Option<&str>, command: &str, args: Vec<&str>, ) -> Result { Ok(Message { tags, prefix: prefix.map(|p| p.into()), command: Command::new(command, args)?, }) } /// Gets the nickname of the message source, if it exists. /// /// # Example /// ``` /// # extern crate irc_proto; /// # use irc_proto::Message; /// # fn main() { /// let message = Message::new( /// Some("nickname!username@hostname"), "JOIN", vec!["#channel"] /// ).unwrap(); /// assert_eq!(message.source_nickname(), Some("nickname")); /// # } /// ``` pub fn source_nickname(&self) -> Option<&str> { // ::= | [ '!' ] [ '@' ] // ::= self.prefix.as_ref().and_then(|p| match p { Prefix::Nickname(name, _, _) => Some(&name[..]), _ => None, }) } /// Gets the likely intended place to respond to this message. /// If the type of the message is a `PRIVMSG` or `NOTICE` and the message is sent to a channel, /// the result will be that channel. In all other cases, this will call `source_nickname`. /// /// # Example /// ``` /// # extern crate irc_proto; /// # use irc_proto::Message; /// # fn main() { /// let msg1 = Message::new( /// Some("ada"), "PRIVMSG", vec!["#channel", "Hi, everyone!"] /// ).unwrap(); /// assert_eq!(msg1.response_target(), Some("#channel")); /// let msg2 = Message::new( /// Some("ada"), "PRIVMSG", vec!["betsy", "betsy: hi"] /// ).unwrap(); /// assert_eq!(msg2.response_target(), Some("ada")); /// # } /// ``` pub fn response_target(&self) -> Option<&str> { match self.command { Command::PRIVMSG(ref target, _) if target.is_channel_name() => Some(target), Command::NOTICE(ref target, _) if target.is_channel_name() => Some(target), _ => self.source_nickname(), } } } impl From for Message { fn from(cmd: Command) -> Message { Message { tags: None, prefix: None, command: cmd, } } } impl FromStr for Message { type Err = ProtocolError; fn from_str(s: &str) -> Result { if s.is_empty() { return Err(ProtocolError::InvalidMessage { string: s.to_owned(), cause: MessageParseError::EmptyMessage, }); } let mut state = s; let tags = if state.starts_with('@') { let tags = state.find(' ').map(|i| &state[1..i]); state = state.find(' ').map_or("", |i| &state[i + 1..]); tags.map(|ts| { ts.split(';') .filter(|s| !s.is_empty()) .map(|s: &str| { let mut iter = s.splitn(2, '='); let (fst, snd) = (iter.next(), iter.next()); let snd = snd.map(unescape_tag_value); Tag(fst.unwrap_or("").to_owned(), snd) }) .collect::>() }) } else { None }; let prefix = if state.starts_with(':') { let prefix = state.find(' ').map(|i| &state[1..i]); state = state.find(' ').map_or("", |i| &state[i + 1..]); prefix } else { None }; let line_ending_len = if state.ends_with("\r\n") { "\r\n" } else if state.ends_with('\r') { "\r" } else if state.ends_with('\n') { "\n" } else { "" } .len(); let suffix = if state.contains(" :") { let suffix = state .find(" :") .map(|i| &state[i + 2..state.len() - line_ending_len]); state = state.find(" :").map_or("", |i| &state[..i + 1]); suffix } else { state = &state[..state.len() - line_ending_len]; None }; let command = match state.find(' ').map(|i| &state[..i]) { Some(cmd) => { state = state.find(' ').map_or("", |i| &state[i + 1..]); cmd } // If there's no arguments but the "command" starts with colon, it's not a command. None if state.starts_with(':') => { return Err(ProtocolError::InvalidMessage { string: s.to_owned(), cause: MessageParseError::InvalidCommand, }) } // If there's no arguments following the command, the rest of the state is the command. None => { let cmd = state; state = ""; cmd } }; let mut args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect(); if let Some(suffix) = suffix { args.push(suffix); } Message::with_tags(tags, prefix, command, args).map_err(|e| ProtocolError::InvalidMessage { string: s.to_owned(), cause: e, }) } } impl<'a> From<&'a str> for Message { fn from(s: &'a str) -> Message { s.parse().unwrap() } } impl Display for Message { /// Converts a Message into a String according to the IRC protocol. /// /// # Example /// ``` /// # extern crate irc_proto; /// # use irc_proto::Message; /// # fn main() { /// let msg = Message::new( /// Some("ada"), "PRIVMSG", vec!["#channel", "Hi, everyone!"] /// ).unwrap(); /// assert_eq!(msg.to_string(), ":ada PRIVMSG #channel :Hi, everyone!\r\n"); /// # } /// ``` fn fmt(&self, f: &mut Formatter) -> FmtResult { if let Some(ref tags) = self.tags { f.write_char('@')?; for (i, tag) in tags.iter().enumerate() { if i > 0 { f.write_char(';')?; } f.write_str(&tag.0)?; if let Some(ref value) = tag.1 { f.write_char('=')?; escape_tag_value(f, value)?; } } f.write_char(' ')?; } if let Some(ref prefix) = self.prefix { write!(f, ":{} ", prefix)? } write!(f, "{}\r\n", String::from(&self.command)) } } /// A message tag as defined by [IRCv3.2](http://ircv3.net/specs/core/message-tags-3.2.html). /// It consists of a tag key, and an optional value for the tag. Each message can contain a number /// of tags (in the string format, they are separated by semicolons). Tags are used to add extended /// information to a message under IRCv3. #[derive(Clone, PartialEq, Debug)] pub struct Tag(pub String, pub Option); fn escape_tag_value(f: &mut dyn Write, value: &str) -> FmtResult { for c in value.chars() { match c { ';' => f.write_str("\\:")?, ' ' => f.write_str("\\s")?, '\\' => f.write_str("\\\\")?, '\r' => f.write_str("\\r")?, '\n' => f.write_str("\\n")?, c => f.write_char(c)?, } } Ok(()) } fn unescape_tag_value(value: &str) -> String { let mut unescaped = String::with_capacity(value.len()); let mut iter = value.chars(); while let Some(c) = iter.next() { let r = if c == '\\' { match iter.next() { Some(':') => ';', Some('s') => ' ', Some('\\') => '\\', Some('r') => '\r', Some('n') => '\n', Some(c) => c, None => break, } } else { c }; unescaped.push(r); } unescaped } #[cfg(test)] mod test { use super::{Message, Tag}; use crate::command::Command::{Raw, PRIVMSG, QUIT}; #[test] fn new() { let message = Message { tags: None, prefix: None, command: PRIVMSG(format!("test"), format!("Testing!")), }; assert_eq!( Message::new(None, "PRIVMSG", vec!["test", "Testing!"]).unwrap(), message ) } #[test] fn source_nickname() { assert_eq!( Message::new(None, "PING", vec!["data"]) .unwrap() .source_nickname(), None ); assert_eq!( Message::new(Some("irc.test.net"), "PING", vec!["data"]) .unwrap() .source_nickname(), None ); assert_eq!( Message::new(Some("test!test@test"), "PING", vec!["data"]) .unwrap() .source_nickname(), Some("test") ); assert_eq!( Message::new(Some("test@test"), "PING", vec!["data"]) .unwrap() .source_nickname(), Some("test") ); assert_eq!( Message::new(Some("test!test@irc.test.com"), "PING", vec!["data"]) .unwrap() .source_nickname(), Some("test") ); assert_eq!( Message::new(Some("test!test@127.0.0.1"), "PING", vec!["data"]) .unwrap() .source_nickname(), Some("test") ); assert_eq!( Message::new(Some("test@test.com"), "PING", vec!["data"]) .unwrap() .source_nickname(), Some("test") ); assert_eq!( Message::new(Some("test"), "PING", vec!["data"]) .unwrap() .source_nickname(), Some("test") ); } #[test] fn to_string() { let message = Message { tags: None, prefix: None, command: PRIVMSG(format!("test"), format!("Testing!")), }; assert_eq!(&message.to_string()[..], "PRIVMSG test Testing!\r\n"); let message = Message { tags: None, prefix: Some("test!test@test".into()), command: PRIVMSG(format!("test"), format!("Still testing!")), }; assert_eq!( &message.to_string()[..], ":test!test@test PRIVMSG test :Still testing!\r\n" ); } #[test] fn from_string() { let message = Message { tags: None, prefix: None, command: PRIVMSG(format!("test"), format!("Testing!")), }; assert_eq!( "PRIVMSG test :Testing!\r\n".parse::().unwrap(), message ); let message = Message { tags: None, prefix: Some("test!test@test".into()), command: PRIVMSG(format!("test"), format!("Still testing!")), }; assert_eq!( ":test!test@test PRIVMSG test :Still testing!\r\n" .parse::() .unwrap(), message ); let message = Message { tags: Some(vec![ Tag(format!("aaa"), Some(format!("bbb"))), Tag(format!("ccc"), None), Tag(format!("example.com/ddd"), Some(format!("eee"))), ]), prefix: Some("test!test@test".into()), command: PRIVMSG(format!("test"), format!("Testing with tags!")), }; assert_eq!( "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \ tags!\r\n" .parse::() .unwrap(), message ) } #[test] fn from_string_atypical_endings() { let message = Message { tags: None, prefix: None, command: PRIVMSG(format!("test"), format!("Testing!")), }; assert_eq!( "PRIVMSG test :Testing!\r".parse::().unwrap(), message ); assert_eq!( "PRIVMSG test :Testing!\n".parse::().unwrap(), message ); assert_eq!( "PRIVMSG test :Testing!".parse::().unwrap(), message ); } #[test] fn from_and_to_string() { let message = "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \ tags!\r\n"; assert_eq!(message.parse::().unwrap().to_string(), message); } #[test] fn to_message() { let message = Message { tags: None, prefix: None, command: PRIVMSG(format!("test"), format!("Testing!")), }; let msg: Message = "PRIVMSG test :Testing!\r\n".into(); assert_eq!(msg, message); let message = Message { tags: None, prefix: Some("test!test@test".into()), command: PRIVMSG(format!("test"), format!("Still testing!")), }; let msg: Message = ":test!test@test PRIVMSG test :Still testing!\r\n".into(); assert_eq!(msg, message); } #[test] fn to_message_with_colon_in_arg() { // Apparently, UnrealIRCd (and perhaps some others) send some messages that include // colons within individual parameters. So, let's make sure it parses correctly. let message = Message { tags: None, prefix: Some("test!test@test".into()), command: Raw( format!("COMMAND"), vec![format!("ARG:test"), format!("Testing!")], ), }; let msg: Message = ":test!test@test COMMAND ARG:test :Testing!\r\n".into(); assert_eq!(msg, message); } #[test] fn to_message_no_prefix_no_args() { let message = Message { tags: None, prefix: None, command: QUIT(None), }; let msg: Message = "QUIT\r\n".into(); assert_eq!(msg, message); } #[test] #[should_panic] fn to_message_invalid_format() { let _: Message = ":invalid :message".into(); } #[test] fn to_message_tags_escapes() { let msg = "@tag=\\:\\s\\\\\\r\\n\\a\\ :test PRIVMSG #test :test\r\n" .parse::() .unwrap(); let message = Message { tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]), prefix: Some("test".into()), command: PRIVMSG("#test".to_string(), "test".to_string()), }; assert_eq!(msg, message); } #[test] fn to_string_tags_escapes() { let msg = Message { tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]), prefix: Some("test".into()), command: PRIVMSG("#test".to_string(), "test".to_string()), } .to_string(); let message = "@tag=\\:\\s\\\\\\r\\na :test PRIVMSG #test test\r\n"; assert_eq!(msg, message); } #[test] fn to_message_with_colon_in_suffix() { let msg = "PRIVMSG #test ::test".parse::().unwrap(); let message = Message { tags: None, prefix: None, command: PRIVMSG("#test".to_string(), ":test".to_string()), }; assert_eq!(msg, message); } #[test] fn to_string_with_colon_in_suffix() { let msg = Message { tags: None, prefix: None, command: PRIVMSG("#test".to_string(), ":test".to_string()), } .to_string(); let message = "PRIVMSG #test ::test\r\n"; assert_eq!(msg, message); } } irc-proto-1.0.0/src/mode.rs000064400000000000000000000230341046102023000136230ustar 00000000000000//! A module defining an API for IRC user and channel modes. use std::fmt; use crate::command::Command; use crate::error::MessageParseError; /// A marker trait for different kinds of Modes. pub trait ModeType: fmt::Display + fmt::Debug + Clone + PartialEq { /// Creates a command of this kind. fn mode(target: &str, modes: &[Mode]) -> Command; /// Returns true if this mode takes an argument, and false otherwise. fn takes_arg(&self) -> bool; /// Creates a Mode from a given char. fn from_char(c: char) -> Self; } /// User modes for the MODE command. #[derive(Clone, Debug, PartialEq)] pub enum UserMode { /// a - user is flagged as away Away, /// i - marks a users as invisible Invisible, /// w - user receives wallops Wallops, /// r - restricted user connection Restricted, /// o - operator flag Oper, /// O - local operator flag LocalOper, /// s - marks a user for receipt of server notices ServerNotices, /// x - masked hostname MaskedHost, /// Any other unknown-to-the-crate mode. Unknown(char), } impl ModeType for UserMode { fn mode(target: &str, modes: &[Mode]) -> Command { Command::UserMODE(target.to_owned(), modes.to_owned()) } fn takes_arg(&self) -> bool { false } fn from_char(c: char) -> UserMode { use self::UserMode::*; match c { 'a' => Away, 'i' => Invisible, 'w' => Wallops, 'r' => Restricted, 'o' => Oper, 'O' => LocalOper, 's' => ServerNotices, 'x' => MaskedHost, _ => Unknown(c), } } } impl fmt::Display for UserMode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::UserMode::*; write!( f, "{}", match *self { Away => 'a', Invisible => 'i', Wallops => 'w', Restricted => 'r', Oper => 'o', LocalOper => 'O', ServerNotices => 's', MaskedHost => 'x', Unknown(c) => c, } ) } } /// Channel modes for the MODE command. #[derive(Clone, Debug, PartialEq)] pub enum ChannelMode { /// b - ban the user from joining or speaking in the channel Ban, /// e - exemptions from bans Exception, /// l - limit the maximum number of users in a channel Limit, /// i - channel becomes invite-only InviteOnly, /// I - exception to invite-only rule InviteException, /// k - specify channel key Key, /// m - channel is in moderated mode Moderated, /// r - entry for registered users only RegisteredOnly, /// s - channel is hidden from listings Secret, /// t - require permissions to edit topic ProtectedTopic, /// n - users must join channels to message them NoExternalMessages, /// q - user gets founder permission Founder, /// a - user gets admin or protected permission Admin, /// o - user gets oper permission Oper, /// h - user gets halfop permission Halfop, /// v - user gets voice permission Voice, /// Any other unknown-to-the-crate mode. Unknown(char), } impl ModeType for ChannelMode { fn mode(target: &str, modes: &[Mode]) -> Command { Command::ChannelMODE(target.to_owned(), modes.to_owned()) } fn takes_arg(&self) -> bool { use self::ChannelMode::*; matches!( *self, Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop | Voice ) } fn from_char(c: char) -> ChannelMode { use self::ChannelMode::*; match c { 'b' => Ban, 'e' => Exception, 'l' => Limit, 'i' => InviteOnly, 'I' => InviteException, 'k' => Key, 'm' => Moderated, 'r' => RegisteredOnly, 's' => Secret, 't' => ProtectedTopic, 'n' => NoExternalMessages, 'q' => Founder, 'a' => Admin, 'o' => Oper, 'h' => Halfop, 'v' => Voice, _ => Unknown(c), } } } impl fmt::Display for ChannelMode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::ChannelMode::*; write!( f, "{}", match *self { Ban => 'b', Exception => 'e', Limit => 'l', InviteOnly => 'i', InviteException => 'I', Key => 'k', Moderated => 'm', RegisteredOnly => 'r', Secret => 's', ProtectedTopic => 't', NoExternalMessages => 'n', Founder => 'q', Admin => 'a', Oper => 'o', Halfop => 'h', Voice => 'v', Unknown(c) => c, } ) } } /// A mode argument for the MODE command. #[derive(Clone, Debug, PartialEq)] pub enum Mode where T: ModeType, { /// Adding the specified mode, optionally with an argument. Plus(T, Option), /// Removing the specified mode, optionally with an argument. Minus(T, Option), /// No prefix mode, used to query ban list on channel join. NoPrefix(T), } impl Mode where T: ModeType, { /// Creates a plus mode with an `&str` argument. pub fn plus(inner: T, arg: Option<&str>) -> Mode { Mode::Plus(inner, arg.map(|s| s.to_owned())) } /// Creates a minus mode with an `&str` argument. pub fn minus(inner: T, arg: Option<&str>) -> Mode { Mode::Minus(inner, arg.map(|s| s.to_owned())) } /// Create a no prefix mode with an `&str` argument. pub fn no_prefix(inner: T) -> Mode { Mode::NoPrefix(inner) } } impl fmt::Display for Mode where T: ModeType, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Mode::Plus(ref mode, Some(ref arg)) => write!(f, "+{} {}", mode, arg), Mode::Minus(ref mode, Some(ref arg)) => write!(f, "-{} {}", mode, arg), Mode::Plus(ref mode, None) => write!(f, "+{}", mode), Mode::Minus(ref mode, None) => write!(f, "-{}", mode), Mode::NoPrefix(ref mode) => write!(f, "{}", mode), } } } enum PlusMinus { Plus, Minus, NoPrefix, } // MODE user [modes] impl Mode { // TODO: turning more edge cases into errors. /// Parses the specified mode string as user modes. pub fn as_user_modes(pieces: &[&str]) -> Result>, MessageParseError> { parse_modes(pieces) } } // MODE channel [modes [modeparams]] impl Mode { // TODO: turning more edge cases into errors. /// Parses the specified mode string as channel modes. pub fn as_channel_modes(pieces: &[&str]) -> Result>, MessageParseError> { parse_modes(pieces) } } fn parse_modes(pieces: &[&str]) -> Result>, MessageParseError> where T: ModeType, { use self::PlusMinus::*; let mut res = vec![]; if let Some((first, rest)) = pieces.split_first() { let mut modes = first.chars(); let mut args = rest.iter(); let mut cur_mod = match modes.next() { Some('+') => Plus, Some('-') => Minus, Some(_) => { // rewind modes modes = first.chars(); NoPrefix } None => { // No modifier return Ok(res); } }; for c in modes { match c { '+' => cur_mod = Plus, '-' => cur_mod = Minus, _ => { let mode = T::from_char(c); let arg = if mode.takes_arg() { // TODO: if there's no arg, this should error args.next() } else { None }; res.push(match cur_mod { Plus => Mode::Plus(mode, arg.map(|s| s.to_string())), Minus => Mode::Minus(mode, arg.map(|s| s.to_string())), NoPrefix => Mode::NoPrefix(mode), }) } } } // TODO: if there are extra args left, this should error } else { // No modifier }; Ok(res) } #[cfg(test)] mod test { use super::{ChannelMode, Mode}; use crate::Command; use crate::Message; #[test] fn parse_channel_mode() { let cmd = "MODE #foo +r".parse::().unwrap().command; assert_eq!( Command::ChannelMODE( "#foo".to_string(), vec![Mode::Plus(ChannelMode::RegisteredOnly, None)] ), cmd ); } #[test] fn parse_no_mode() { let cmd = "MODE #foo".parse::().unwrap().command; assert_eq!(Command::ChannelMODE("#foo".to_string(), vec![]), cmd); } #[test] fn parse_no_plus() { let cmd = "MODE #foo b".parse::().unwrap().command; assert_eq!( Command::ChannelMODE("#foo".to_string(), vec![Mode::NoPrefix(ChannelMode::Ban)]), cmd ); } } irc-proto-1.0.0/src/prefix.rs000064400000000000000000000136661046102023000142060ustar 00000000000000//! A module providing an enum for a message prefix. use std::fmt; use std::str::FromStr; /// The Prefix indicates "the true origin of the message", according to the server. #[derive(Clone, Eq, PartialEq, Debug)] pub enum Prefix { /// servername, e.g. collins.mozilla.org ServerName(String), /// nickname [ ["!" username] "@" hostname ] /// i.e. Nickname(nickname, username, hostname) /// Any of the strings may be "" Nickname(String, String, String), } impl Prefix { /// Creates a prefix by parsing a string. /// /// # Example /// ``` /// # extern crate irc_proto; /// # use irc_proto::Prefix; /// # fn main() { /// Prefix::new_from_str("nickname!username@hostname"); /// Prefix::new_from_str("example.com"); /// # } /// ``` pub fn new_from_str(s: &str) -> Prefix { #[derive(Copy, Clone, Eq, PartialEq)] enum Active { Name, User, Host, } let mut name = String::new(); let mut user = String::new(); let mut host = String::new(); let mut active = Active::Name; let mut is_server = false; for c in s.chars() { if c == '.' && active == Active::Name { // We won't return Nickname("nick", "", "") but if @ or ! are // encountered, then we set this back to false is_server = true; } match c { '!' if active == Active::Name => { is_server = false; active = Active::User; } '@' if active != Active::Host => { is_server = false; active = Active::Host; } _ => { // Push onto the active buffer match active { Active::Name => &mut name, Active::User => &mut user, Active::Host => &mut host, } .push(c) } } } if is_server { Prefix::ServerName(name) } else { Prefix::Nickname(name, user, host) } } } /// This implementation never returns an error and is isomorphic with `Display`. impl FromStr for Prefix { type Err = (); fn from_str(s: &str) -> Result { Ok(Prefix::new_from_str(s)) } } /// This is isomorphic with `FromStr` impl fmt::Display for Prefix { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Prefix::ServerName(name) => write!(f, "{}", name), Prefix::Nickname(name, user, host) => match (&name[..], &user[..], &host[..]) { ("", "", "") => write!(f, ""), (name, "", "") => write!(f, "{}", name), (name, user, "") => write!(f, "{}!{}", name, user), (name, "", host) => write!(f, "{}@{}", name, host), (name, user, host) => write!(f, "{}!{}@{}", name, user, host), }, } } } impl<'a> From<&'a str> for Prefix { fn from(s: &str) -> Self { Prefix::new_from_str(s) } } #[cfg(test)] mod test { use super::Prefix::{self, Nickname, ServerName}; // Checks that str -> parsed -> Display doesn't lose data fn test_parse(s: &str) -> Prefix { let prefix = Prefix::new_from_str(s); let s2 = format!("{}", prefix); assert_eq!(s, &s2); prefix } #[test] fn print() { let s = format!("{}", Nickname("nick".into(), "".into(), "".into())); assert_eq!(&s, "nick"); let s = format!("{}", Nickname("nick".into(), "user".into(), "".into())); assert_eq!(&s, "nick!user"); let s = format!("{}", Nickname("nick".into(), "user".into(), "host".into())); assert_eq!(&s, "nick!user@host"); } #[test] fn parse_word() { assert_eq!( test_parse("only_nick"), Nickname("only_nick".into(), String::new(), String::new()) ) } #[test] fn parse_host() { assert_eq!(test_parse("host.tld"), ServerName("host.tld".into())) } #[test] fn parse_nick_user() { assert_eq!( test_parse("test!nick"), Nickname("test".into(), "nick".into(), String::new()) ) } #[test] fn parse_nick_user_host() { assert_eq!( test_parse("test!nick@host"), Nickname("test".into(), "nick".into(), "host".into()) ) } #[test] fn parse_dot_and_symbols() { assert_eq!( test_parse("test.net@something"), Nickname("test.net".into(), "".into(), "something".into()) ) } #[test] fn parse_danger_cases() { assert_eq!( test_parse("name@name!user"), Nickname("name".into(), "".into(), "name!user".into()) ); assert_eq!( // can't reverse the parse "name!@".parse::().unwrap(), Nickname("name".into(), "".into(), "".into()) ); assert_eq!( // can't reverse the parse "name!@hostname".parse::().unwrap(), Nickname("name".into(), "".into(), "hostname".into()) ); assert_eq!( test_parse("name!.user"), Nickname("name".into(), ".user".into(), "".into()) ); assert_eq!( test_parse("name!user.user"), Nickname("name".into(), "user.user".into(), "".into()) ); assert_eq!( test_parse("name!user@host.host"), Nickname("name".into(), "user".into(), "host.host".into()) ); assert_eq!( test_parse("!user"), Nickname("".into(), "user".into(), "".into()) ); assert_eq!( "!@host.host".parse::().unwrap(), Nickname("".into(), "".into(), "host.host".into()) ); } } irc-proto-1.0.0/src/response.rs000064400000000000000000000504761046102023000145470ustar 00000000000000//! Enumeration of all the possible server responses. #![allow(non_camel_case_types)] use std::str::FromStr; macro_rules! make_response { ($($(#[$attr:meta])+ $variant:ident = $value:expr),+) => { /// List of all server responses as defined in /// [RFC 2812](http://tools.ietf.org/html/rfc2812) and /// [Modern docs](https://modern.ircdocs.horse/#numerics) (henceforth referred to as /// Modern). All commands are documented with their expected form from the RFC, and any /// useful, additional information about the response code. #[derive(Clone, Copy, Debug, PartialEq)] #[repr(u16)] pub enum Response { $($(#[$attr])+ $variant = $value),+ } impl Response { /// Generates a Response from a u16. fn from_u16(val: u16) -> Option { match val { $($value => Some(Response::$variant),)+ _ => None } } } } } make_response! { // Expected replies /// `001 Welcome to the Internet Relay Network !@` (Source: RFC2812) RPL_WELCOME = 1, /// `002 Your host is , running version ` (Source: RFC2812) RPL_YOURHOST = 2, /// `003 This server was created ` (Source: RFC2812) RPL_CREATED = 3, /// `004 ` (Source: /// RFC2812) /// /// Various IRCds may choose to include additional arguments to `RPL_MYINFO`, and it's best to /// check for certain what the servers you're targeting do. Typically, there are additional /// parameters at the end for modes that have parameters, and server modes. RPL_MYINFO = 4, /// `005 *((=)) :are supported by this server` (Source: Modern) /// /// [RPL_ISUPPORT](https://modern.ircdocs.horse/#rplisupport-005) replaces RPL_BOUNCE from /// RFC2812, but does so consistently in modern IRCd implementations. RPL_BOUNCE has been moved /// to `010`. RPL_ISUPPORT = 5, /// `010 Try server , port ` (Source: Modern) RPL_BOUNCE = 10, /// Undefined format. (Source: Modern) /// /// RPL_NONE is a dummy numeric. It does not have a defined use nor format. RPL_NONE = 300, /// `302 :*1 *( " " )` (Source: RFC2812) RPL_USERHOST = 302, /// `303 :*1 *( " " )` (Source: RFC2812) RPL_ISON = 303, /// `301 :` (Source: RFC2812) RPL_AWAY = 301, /// `305 :You are no longer marked as being away` (Source: RFC2812) RPL_UNAWAY = 305, /// `306 :You have been marked as being away` (Source: RFC2812) RPL_NOWAWAY = 306, /// `311 * :` (Source: RFC2812) RPL_WHOISUSER = 311, /// `312 :` (Source: RFC2812) RPL_WHOISSERVER = 312, /// `313 :is an IRC operator` (Source: RFC2812) RPL_WHOISOPERATOR = 313, /// `317 :seconds idle` (Source: RFC2812) RPL_WHOISIDLE = 317, /// `318 :End of WHOIS list` (Source: RFC2812) RPL_ENDOFWHOIS = 318, /// `319 :*( ( "@" / "+" ) " " )` (Source: RFC2812) RPL_WHOISCHANNELS = 319, /// `314 * :` (Source: RFC2812) RPL_WHOWASUSER = 314, /// `369 :End of WHOWAS` (Source: RFC2812) RPL_ENDOFWHOWAS = 369, /// Obsolete. Not used. (Source: RFC2812) RPL_LISTSTART = 321, /// `322 <# visible> :` (Source: RFC2812) RPL_LIST = 322, /// `323 :End of LIST (Source: RFC2812) RPL_LISTEND = 323, /// `325 ` (Source: RFC2812) RPL_UNIQOPIS = 325, /// `324 ` (Source: RFC2812) RPL_CHANNELMODEIS = 324, /// `331 :No topic is set` (Source: RFC2812) RPL_NOTOPIC = 331, /// `332 :` (Source: RFC2812) RPL_TOPIC = 332, /// `333 !@ ` (Source: RFC2812) RPL_TOPICWHOTIME = 333, /// `341 ` (Source: RFC2812) RPL_INVITING = 341, /// `342 :Summoning user to IRC` (Source: RFC2812) /// /// According to Modern, this response is rarely implemented. In practice, people simply message /// one another in a channel with their specified username in the message, rather than use the /// `SUMMON` command. RPL_SUMMONING = 342, /// `346 ` (Source: RFC2812) RPL_INVITELIST = 346, /// `347 :End of channel invite list` (Source: RFC2812) /// /// According to Modern, `RPL_ENDOFEXCEPTLIST` (349) is frequently deployed for this same /// purpose and the difference will be noted in channel mode and the statement in the suffix. RPL_ENDOFINVITELIST = 347, /// `348 ` (Source: RFC2812) RPL_EXCEPTLIST = 348, /// `349 :End of channel exception list` (Source: RFC2812) RPL_ENDOFEXCEPTLIST = 349, /// `351 :` (Source: RFC2812/Modern) RPL_VERSION = 351, /// `352 ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] /// : ` (Source: RFC2812) RPL_WHOREPLY = 352, /// `315 :End of WHO list` (Source: RFC2812) RPL_ENDOFWHO = 315, /// `353 ( "=" / "*" / "@" ) :[ "@" / "+" ] *( " " [ "@" / "+" ] )` /// (Source: RFC2812) RPL_NAMREPLY = 353, /// `366 :End of NAMES list` (Source: RFC2812) RPL_ENDOFNAMES = 366, /// `364 : ` (Source: RFC2812) RPL_LINKS = 364, /// `365 :End of LINKS list` (Source: RFC2812) RPL_ENDOFLINKS = 365, /// `367 ` (Source: RFC2812) RPL_BANLIST = 367, /// `368 :End of channel ban list` (Source: RFC2812) RPL_ENDOFBANLIST = 368, /// `371 :` (Source: RFC2812) RPL_INFO = 371, /// `374 :End of INFO list` (Source: RFC2812) RPL_ENDOFINFO = 374, /// `375 :- Message of the day -` (Source: RFC2812) RPL_MOTDSTART = 375, /// `372 :- ` (Source: RFC2812) RPL_MOTD = 372, /// `376 :End of MOTD command` (Source: RFC2812) RPL_ENDOFMOTD = 376, /// `381 :You are now an IRC operator` (Source: RFC2812) RPL_YOUREOPER = 381, /// `382 :Rehashing` (Source: RFC2812) RPL_REHASHING = 382, /// `383 You are service ` (Source: RFC2812) RPL_YOURESERVICE = 383, /// `391 :` (Source: RFC2812) RPL_TIME = 391, /// `392 :UserID Terminal Host` (Source: RFC2812) RPL_USERSSTART = 392, /// `393 : ` (Source: RFC2812) RPL_USERS = 393, /// `394 :End of users` (Source: RFC2812) RPL_ENDOFUSERS = 394, /// `395 :Nobody logged in` (Source: RFC2812) RPL_NOUSERS = 395, /// `396 :is now your displayed host` (Source: InspIRCd) /// /// This response code is sent after a user enables the user mode +x (host masking), and it is /// successfully enabled. The particular format described above is from InspIRCd, but the /// response code should be common amongst servers that support host masks. RPL_HOSTHIDDEN = 396, /// `200 Link V /// ` (Source: RFC2812) RPL_TRACELINK = 200, /// `201 Try. ` (Source: RFC2812) RPL_TRACECONNECTING = 201, /// `202 H.S. ` (Source: RFC2812) RPL_TRACEHANDSHAKE = 202, /// `203 ???? []` (Source: RFC2812) RPL_TRACEUKNOWN = 203, /// `204 Oper ` (Source: RFC2812) RPL_TRACEOPERATOR = 204, /// `205 User ` (Source: RFC2812) RPL_TRACEUSER = 205, /// `206 Serv S C @ V` /// (Source: RFC2812) RPL_TRACESERVER = 206, /// `207 Service ` (Source: RFC2812) RPL_TRACESERVICE = 207, /// `208 0 ` (Source: RFC2812) RPL_TRACENEWTYPE = 208, /// `209 Class ` (Source: RFC2812) RPL_TRACECLASS = 209, /// Unused. (Source: RFC2812) RPL_TRACERECONNECT = 210, /// `261 File ` (Source: RFC2812) RPL_TRACELOG = 261, /// `262 :End of TRACE` (Source: RFC2812) RPL_TRACEEND = 262, /// `211 ///