From d9f4f8205134e191bb844f78eb3fd60920a879c9 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Thu, 22 Jun 2017 13:59:18 -0400 Subject: [PATCH] Implemented new Mode API (fixes #48). --- src/client/data/config.rs | 2 +- src/client/data/user.rs | 55 +++--- src/client/server/mod.rs | 28 ++- src/client/server/utils.rs | 30 ++- src/error.rs | 5 + src/proto/command.rs | 362 +++++++++++++++++++++++++++++++++---- src/proto/mod.rs | 2 +- 7 files changed, 396 insertions(+), 88 deletions(-) diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 1bc8384..c0e108e 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -42,7 +42,7 @@ pub struct Config { pub channels: Option>, /// A mapping of channel names to keys for join-on-connect. pub channel_keys: Option>, - /// User modes to set on connect. Example: "+RB-x" + /// User modes to set on connect. Example: "+RB -x" pub umodes: Option, /// The text that'll be sent in response to CTCP USERINFO requests. pub user_info: Option, diff --git a/src/client/data/user.rs b/src/client/data/user.rs index 62341f9..71b5088 100644 --- a/src/client/data/user.rs +++ b/src/client/data/user.rs @@ -3,6 +3,7 @@ use std::borrow::ToOwned; use std::cmp::Ordering; use std::cmp::Ordering::{Less, Equal, Greater}; use std::str::FromStr; +use proto::{Mode, ChannelMode}; /// IRC User data. #[derive(Clone, Debug)] @@ -77,18 +78,18 @@ impl User { } /// Updates the user's access level. - pub fn update_access_level(&mut self, mode: &str) { + pub fn update_access_level(&mut self, mode: &Mode) { match mode { - "+q" => self.add_access_level(AccessLevel::Owner), - "-q" => self.sub_access_level(AccessLevel::Owner), - "+a" => self.add_access_level(AccessLevel::Admin), - "-a" => self.sub_access_level(AccessLevel::Admin), - "+o" => self.add_access_level(AccessLevel::Oper), - "-o" => self.sub_access_level(AccessLevel::Oper), - "+h" => self.add_access_level(AccessLevel::HalfOp), - "-h" => self.sub_access_level(AccessLevel::HalfOp), - "+v" => self.add_access_level(AccessLevel::Voice), - "-v" => self.sub_access_level(AccessLevel::Voice), + &Mode::Plus(ChannelMode::Founder, _) => self.add_access_level(AccessLevel::Owner), + &Mode::Minus(ChannelMode::Founder, _) => self.sub_access_level(AccessLevel::Owner), + &Mode::Plus(ChannelMode::Admin, _) => self.add_access_level(AccessLevel::Admin), + &Mode::Minus(ChannelMode::Admin, _) => self.sub_access_level(AccessLevel::Admin), + &Mode::Plus(ChannelMode::Oper, _) => self.add_access_level(AccessLevel::Oper), + &Mode::Minus(ChannelMode::Oper, _) => self.sub_access_level(AccessLevel::Oper), + &Mode::Plus(ChannelMode::Halfop, _) => self.add_access_level(AccessLevel::HalfOp), + &Mode::Minus(ChannelMode::Halfop, _) => self.sub_access_level(AccessLevel::HalfOp), + &Mode::Plus(ChannelMode::Voice, _) => self.add_access_level(AccessLevel::Voice), + &Mode::Minus(ChannelMode::Voice, _) => self.sub_access_level(AccessLevel::Voice), _ => {} } } @@ -225,6 +226,8 @@ impl Iterator for AccessLevelIterator { mod test { use super::{AccessLevel, User}; use super::AccessLevel::*; + use proto::Mode::*; + use proto::ChannelMode as M; #[test] fn parse_access_level() { @@ -300,25 +303,25 @@ mod test { fn update_user_rank() { let mut user = User::new("user"); assert_eq!(user.highest_access_level, Member); - user.update_access_level("+q"); + user.update_access_level(&Plus(M::Founder, None)); assert_eq!(user.highest_access_level, Owner); - user.update_access_level("-q"); + user.update_access_level(&Minus(M::Founder, None)); assert_eq!(user.highest_access_level, Member); - user.update_access_level("+a"); + user.update_access_level(&Plus(M::Admin, None)); assert_eq!(user.highest_access_level, Admin); - user.update_access_level("-a"); + user.update_access_level(&Minus(M::Admin, None)); assert_eq!(user.highest_access_level, Member); - user.update_access_level("+o"); + user.update_access_level(&Plus(M::Oper, None)); assert_eq!(user.highest_access_level, Oper); - user.update_access_level("-o"); + user.update_access_level(&Minus(M::Oper, None)); assert_eq!(user.highest_access_level, Member); - user.update_access_level("+h"); + user.update_access_level(&Plus(M::Halfop, None)); assert_eq!(user.highest_access_level, HalfOp); - user.update_access_level("-h"); + user.update_access_level(&Minus(M::Halfop, None)); assert_eq!(user.highest_access_level, Member); - user.update_access_level("+v"); + user.update_access_level(&Plus(M::Voice, None)); assert_eq!(user.highest_access_level, Voice); - user.update_access_level("-v"); + user.update_access_level(&Minus(M::Voice, None)); assert_eq!(user.highest_access_level, Member); } @@ -330,19 +333,19 @@ mod test { user.access_levels, vec![Owner, Admin, Oper, HalfOp, Voice, Member] ); - user.update_access_level("-h"); + user.update_access_level(&Minus(M::Halfop, None)); assert_eq!(user.highest_access_level, Owner); assert_eq!(user.access_levels, vec![Owner, Admin, Oper, Member, Voice]); - user.update_access_level("-q"); + user.update_access_level(&Minus(M::Founder, None)); assert_eq!(user.highest_access_level, Admin); assert_eq!(user.access_levels, vec![Voice, Admin, Oper, Member]); - user.update_access_level("-a"); + user.update_access_level(&Minus(M::Admin, None)); assert_eq!(user.highest_access_level, Oper); assert_eq!(user.access_levels, vec![Voice, Member, Oper]); - user.update_access_level("-o"); + user.update_access_level(&Minus(M::Oper, None)); assert_eq!(user.highest_access_level, Voice); assert_eq!(user.access_levels, vec![Voice, Member]); - user.update_access_level("-v"); + user.update_access_level(&Minus(M::Voice, None)); assert_eq!(user.highest_access_level, Member); assert_eq!(user.access_levels, vec![Member]); } diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 802633a..9564ca0 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -9,8 +9,8 @@ use client::conn::Connection; use client::data::{Config, User}; use client::server::utils::ServerExt; use client::transport::LogView; -use proto::{Command, Message, Response}; -use proto::Command::{JOIN, NICK, NICKSERV, PART, PRIVMSG, MODE, QUIT}; +use proto::{ChannelMode, Command, Message, Mode, Response}; +use proto::Command::{JOIN, NICK, NICKSERV, PART, PRIVMSG, ChannelMODE, QUIT}; use futures::{Async, Poll, Future, Sink, Stream}; use futures::stream::SplitStream; use futures::sync::mpsc; @@ -206,7 +206,7 @@ impl ServerState { NICK(ref new_nick) => { self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick) } - MODE(ref chan, ref mode, Some(ref user)) => self.handle_mode(chan, mode, user), + ChannelMODE(ref chan, ref modes) => self.handle_mode(chan, modes), PRIVMSG(ref target, ref body) => { if body.starts_with('\u{001}') { let tokens: Vec<_> = { @@ -290,7 +290,7 @@ impl ServerState { if self.config().umodes().is_empty() { Ok(()) } else { - self.send_mode(self.current_nickname(), self.config().umodes(), "") + self.send_mode(self.current_nickname(), &Mode::as_user_modes(self.config().umodes())?) } } @@ -358,13 +358,20 @@ impl ServerState { } #[cfg(feature = "nochanlists")] - fn handle_mode(&self, chan: &str, mode: &str, user: &str) {} + fn handle_mode(&self, _: &str, _: &[Mode]) {} #[cfg(not(feature = "nochanlists"))] - fn handle_mode(&self, chan: &str, mode: &str, user: &str) { - if let Some(vec) = self.chanlists.lock().unwrap().get_mut(chan) { - if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) { - vec[n].update_access_level(mode) + fn handle_mode(&self, chan: &str, modes: &[Mode]) { + for mode in modes { + match mode { + &Mode::Plus(_, Some(ref user)) | &Mode::Minus(_, Some(ref user)) => { + if let Some(vec) = self.chanlists.lock().unwrap().get_mut(chan) { + if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) { + vec[n].update_access_level(mode) + } + } + } + _ => (), } } } @@ -556,6 +563,7 @@ mod test { use client::data::Config; #[cfg(not(feature = "nochanlists"))] use client::data::User; + use proto::{ChannelMode, Mode}; use proto::command::Command::{PART, PRIVMSG}; use futures::{Future, Stream}; @@ -893,7 +901,7 @@ mod test { vec![User::new("@test"), User::new("~owner"), User::new("&admin")] ); let mut exp = User::new("@test"); - exp.update_access_level("+v"); + exp.update_access_level(&Mode::Plus(ChannelMode::Voice, None)); assert_eq!( server.list_users("#test").unwrap()[0].highest_access_level(), exp.highest_access_level() diff --git a/src/client/server/utils.rs b/src/client/server/utils.rs index 467b249..b23ea89 100644 --- a/src/client/server/utils.rs +++ b/src/client/server/utils.rs @@ -1,9 +1,9 @@ //! Utilities and shortcuts for working with IRC servers. use std::borrow::ToOwned; use error::Result; -use proto::{Capability, NegotiationVersion}; -use proto::Command::{AUTHENTICATE, CAP, INVITE, JOIN, KICK, KILL, MODE, NICK, NOTICE}; -use proto::Command::{OPER, PART, PASS, PONG, PRIVMSG, QUIT, SAMODE, SANICK, TOPIC, USER}; +use proto::{Capability, Command, Mode, NegotiationVersion}; +use proto::command::Command::*; +use proto::command::ModeType; use client::server::Server; use proto::command::CapSubCommand::{END, LS, REQ}; #[cfg(feature = "ctcp")] @@ -16,7 +16,7 @@ pub trait ServerExt: Server { where Self: Sized, { - self.send(CAP( + self.send(Command::CAP( None, LS, match version { @@ -201,21 +201,13 @@ pub trait ServerExt: Server { )) } - /// Changes the mode of the target. - /// If `modeparmas` is an empty string, it won't be included in the message. - fn send_mode(&self, target: &str, mode: &str, modeparams: &str) -> Result<()> + /// Changes the modes for the specified target. + fn send_mode(&self, target: &str, modes: &[Mode]) -> Result<()> where Self: Sized, + T: ModeType, { - self.send(MODE( - target.to_owned(), - mode.to_owned(), - if modeparams.is_empty() { - None - } else { - Some(modeparams.to_owned()) - }, - )) + self.send(T::mode(target, modes)) } /// Changes the mode of the target by force. @@ -358,6 +350,7 @@ mod test { use client::data::Config; use client::server::IrcServer; use client::server::test::{get_server_value, test_config}; + use proto::{ChannelMode, Mode}; #[test] fn identify() { @@ -483,14 +476,15 @@ mod test { #[test] fn send_mode_no_modeparams() { let server = IrcServer::from_config(test_config()).unwrap(); - server.send_mode("#test", "+i", "").unwrap(); + server.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap(); assert_eq!(&get_server_value(server)[..], "MODE #test +i\r\n"); } #[test] fn send_mode() { let server = IrcServer::from_config(test_config()).unwrap(); - server.send_mode("#test", "+o", "test").unwrap(); + server.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))]) + .unwrap(); assert_eq!(&get_server_value(server)[..], "MODE #test +o test\r\n"); } diff --git a/src/error.rs b/src/error.rs index 6a5d256..1fcb4c3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -30,6 +30,11 @@ error_chain! { display("Failed to parse an IRC subcommand.") } + ModeParsingFailed { + description("Failed to parse a mode correctly.") + display("Failed to parse a mode correctly.") + } + /// An error occurred on one of the internal channels of the `IrcServer`. ChannelError { description("An error occured on one of the IrcServer's internal channels.") diff --git a/src/proto/command.rs b/src/proto/command.rs index 24c3fed..4162480 100644 --- a/src/proto/command.rs +++ b/src/proto/command.rs @@ -1,5 +1,6 @@ //! Enumeration of all available client commands. use std::ascii::AsciiExt; +use std::fmt; use std::str::FromStr; use error; use proto::Response; @@ -20,8 +21,7 @@ pub enum Command { /// OPER name :password OPER(String, String), /// MODE nickname modes - /// MODE channel modes [modeparams] - MODE(String, String, Option), + UserMODE(String, Vec>), /// SERVICE nickname reserved distribution type reserved :info SERVICE(String, String, String, String, String, String), /// QUIT :comment @@ -34,8 +34,8 @@ pub enum Command { JOIN(String, Option, Option), /// PART chanlist :[comment] PART(String, Option), - // MODE is already defined. - // MODE(String, String, Option), + /// MODE channel [modes [modeparams]] + ChannelMODE(String, Vec>), /// TOPIC channel :[topic] TOPIC(String, Option), /// NAMES [chanlist :[target]] @@ -191,8 +191,13 @@ impl<'a> From<&'a Command> for String { Command::NICK(ref n) => stringify("NICK", &[], Some(n)), Command::USER(ref u, ref m, ref r) => stringify("USER", &[u, m, "*"], Some(r)), Command::OPER(ref u, ref p) => stringify("OPER", &[u], Some(p)), - Command::MODE(ref t, ref m, Some(ref p)) => stringify("MODE", &[t, m, p], None), - Command::MODE(ref t, ref m, None) => stringify("MODE", &[t, m], None), + Command::UserMODE(ref u, ref m) => { + format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| { + acc.push_str(" "); + acc.push_str(&mode.to_string()); + acc + })) + } Command::SERVICE(ref n, ref r, ref d, ref t, ref re, ref i) => { stringify("SERVICE", &[n, r, d, t, re], Some(i)) } @@ -205,6 +210,13 @@ impl<'a> From<&'a Command> for String { Command::JOIN(ref c, None, None) => stringify("JOIN", &[c], None), Command::PART(ref c, Some(ref m)) => stringify("PART", &[c], Some(m)), Command::PART(ref c, None) => stringify("PART", &[c], None), + Command::ChannelMODE(ref u, ref m) => { + format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| { + acc.push_str(" "); + acc.push_str(&mode.to_string()); + acc + })) + } Command::TOPIC(ref c, Some(ref t)) => stringify("TOPIC", &[c], Some(t)), Command::TOPIC(ref c, None) => stringify("TOPIC", &[c], None), Command::NAMES(Some(ref c), Some(ref t)) => stringify("NAMES", &[c], Some(t)), @@ -505,32 +517,14 @@ impl Command { } } else if cmd.eq_ignore_ascii_case("MODE") { match suffix { - Some(suffix) => { - if args.len() == 2 { - Command::MODE( - args[0].to_owned(), - args[1].to_owned(), - Some(suffix.to_owned()), - ) - } else if args.len() == 1 { - Command::MODE(args[0].to_owned(), suffix.to_owned(), None) - } else { - raw(cmd, args, Some(suffix)) - } - } - None => { - if args.len() == 3 { - Command::MODE( - args[0].to_owned(), - args[1].to_owned(), - Some(args[2].to_owned()), - ) - } else if args.len() == 2 { - Command::MODE(args[0].to_owned(), args[1].to_owned(), None) - } else { - raw(cmd, args, suffix) - } - } + Some(suffix) => raw(cmd, args, Some(suffix)), + None => if args[0].is_channel_name() { + let arg = args[1..].join(" "); + Command::ChannelMODE(args[0].to_owned(), Mode::as_channel_modes(&arg)?) + } else { + let arg = args[1..].join(" "); + Command::UserMODE(args[0].to_owned(), Mode::as_user_modes(&arg)?) + }, } } else if cmd.eq_ignore_ascii_case("SERVICE") { match suffix { @@ -1760,3 +1754,307 @@ impl FromStr for BatchSubCommand { } } } + +/// 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; +} + +/// 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, + + /// 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 + } +} + +impl UserMode { + fn from_char(c: char) -> error::Result { + use self::UserMode::*; + + Ok(match c { + 'a' => Away, + 'i' => Invisible, + 'w' => Wallops, + 'r' => Restricted, + 'o' => Oper, + 'O' => LocalOper, + 's' => ServerNotices, + _ => 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', + 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, + /// 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::*; + + match *self { + Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop | + Voice => true, + _ => false, + } + } +} + +impl ChannelMode { + fn from_char(c: char) -> error::Result { + use self::ChannelMode::*; + + Ok(match c { + 'b' => Ban, + 'e' => Exception, + 'l' => Limit, + 'i' => InviteOnly, + 'I' => InviteException, + 'k' => Key, + 'm' => Moderated, + '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', + 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), +} + +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), + } + } +} + +enum PlusMinus { + Plus, + Minus, +} + +// 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(s: &str) -> error::Result>> { + use self::PlusMinus::*; + + let mut res = vec![]; + let mut pieces = s.split(" "); + for term in pieces.clone() { + if term.starts_with("+") || term.starts_with("-") { + let _ = pieces.next(); + + let mut chars = term.chars(); + let init = match chars.next() { + Some('+') => Plus, + Some('-') => Minus, + _ => return Err(error::ErrorKind::ModeParsingFailed.into()), + }; + + for c in chars { + let mode = UserMode::from_char(c)?; + let arg = if mode.takes_arg() { + pieces.next() + } else { + None + }; + res.push(match init { + Plus => Mode::Plus(mode, arg.map(|s| s.to_owned())), + Minus => Mode::Minus(mode, arg.map(|s| s.to_owned())), + }) + } + } + } + + Ok(res) + } +} + +// 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(s: &str) -> error::Result>> { + use self::PlusMinus::*; + + let mut res = vec![]; + let mut pieces = s.split(" "); + for term in pieces.clone() { + if term.starts_with("+") || term.starts_with("-") { + let _ = pieces.next(); + + let mut chars = term.chars(); + let init = match chars.next() { + Some('+') => Plus, + Some('-') => Minus, + _ => return Err(error::ErrorKind::ModeParsingFailed.into()), + }; + + for c in chars { + let mode = ChannelMode::from_char(c)?; + let arg = if mode.takes_arg() { + pieces.next() + } else { + None + }; + res.push(match init { + Plus => Mode::Plus(mode, arg.map(|s| s.to_owned())), + Minus => Mode::Minus(mode, arg.map(|s| s.to_owned())), + }) + } + } + } + + Ok(res) + } +} + +/// 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("!") + } +} diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 27a5085..b505fb8 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -8,7 +8,7 @@ pub mod message; pub mod response; pub use self::caps::{Capability, NegotiationVersion}; -pub use self::command::{BatchSubCommand, CapSubCommand, Command}; +pub use self::command::{BatchSubCommand, CapSubCommand, ChannelMode, Command, Mode, UserMode}; pub use self::irc::IrcCodec; pub use self::message::Message; pub use self::response::Response;