From c9a767c8ff8295944edf1b88f2bde473195d9bd8 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Tue, 13 Jan 2015 02:39:59 -0500 Subject: [PATCH] Refactored library in preparation for server work. --- examples/autoreconnect.rs | 6 +- examples/multithreaded.rs | 6 +- examples/simple.rs | 6 +- examples/simple_ssl.rs | 6 +- examples/tweeter.rs | 6 +- src/{ => client}/conn.rs | 6 +- src/{ => client}/data/command.rs | 2 +- src/{ => client}/data/config.rs | 0 src/{ => client}/data/message.rs | 0 src/{ => client}/data/mod.rs | 10 +- src/{ => client}/data/response.rs | 4 +- src/{ => client}/data/user.rs | 2 +- src/client/mod.rs | 6 + src/client/server/mod.rs | 542 +++++++++++++++++++++++++++++ src/{ => client}/server/utils.rs | 20 +- src/lib.rs | 5 +- src/server/mod.rs | 544 +----------------------------- 17 files changed, 589 insertions(+), 582 deletions(-) rename src/{ => client}/conn.rs (99%) rename src/{ => client}/data/command.rs (99%) rename src/{ => client}/data/config.rs (100%) rename src/{ => client}/data/message.rs (100%) rename src/{ => client}/data/mod.rs (80%) rename src/{ => client}/data/response.rs (99%) rename src/{ => client}/data/user.rs (99%) create mode 100644 src/client/mod.rs create mode 100644 src/client/server/mod.rs rename src/{ => client}/server/utils.rs (97%) diff --git a/examples/autoreconnect.rs b/examples/autoreconnect.rs index 8a852d6..ab21160 100644 --- a/examples/autoreconnect.rs +++ b/examples/autoreconnect.rs @@ -5,9 +5,9 @@ extern crate irc; use std::default::Default; use std::sync::Arc; use std::thread::Thread; -use irc::data::{Command, Config}; -use irc::server::{IrcServer, Server}; -use irc::server::utils::Wrapper; +use irc::client::data::{Command, Config}; +use irc::client::server::{IrcServer, Server}; +use irc::client::server::utils::Wrapper; fn main() { let config = Config { diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 3a4e820..9c06ff7 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -5,9 +5,9 @@ extern crate irc; use std::default::Default; use std::sync::Arc; use std::thread::Thread; -use irc::data::config::Config; -use irc::server::{IrcServer, Server}; -use irc::server::utils::Wrapper; +use irc::client::data::config::Config; +use irc::client::server::{IrcServer, Server}; +use irc::client::server::utils::Wrapper; fn main() { let config = Config { diff --git a/examples/simple.rs b/examples/simple.rs index 8eb2174..1c2ff52 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -3,9 +3,9 @@ extern crate irc; use std::default::Default; -use irc::data::config::Config; -use irc::server::{IrcServer, Server}; -use irc::server::utils::Wrapper; +use irc::client::data::config::Config; +use irc::client::server::{IrcServer, Server}; +use irc::client::server::utils::Wrapper; fn main() { let config = Config { diff --git a/examples/simple_ssl.rs b/examples/simple_ssl.rs index 7786bea..545c2b4 100644 --- a/examples/simple_ssl.rs +++ b/examples/simple_ssl.rs @@ -3,9 +3,9 @@ extern crate irc; use std::default::Default; -use irc::data::config::Config; -use irc::server::{IrcServer, Server}; -use irc::server::utils::Wrapper; +use irc::client::data::config::Config; +use irc::client::server::{IrcServer, Server}; +use irc::client::server::utils::Wrapper; fn main() { let config = Config { diff --git a/examples/tweeter.rs b/examples/tweeter.rs index 08408bd..0d259c3 100644 --- a/examples/tweeter.rs +++ b/examples/tweeter.rs @@ -7,9 +7,9 @@ use std::io::timer::sleep; use std::sync::Arc; use std::thread::Thread; use std::time::duration::Duration; -use irc::data::config::Config; -use irc::server::{IrcServer, Server}; -use irc::server::utils::Wrapper; +use irc::client::data::config::Config; +use irc::client::server::{IrcServer, Server}; +use irc::client::server::utils::Wrapper; fn main() { let config = Config { diff --git a/src/conn.rs b/src/client/conn.rs similarity index 99% rename from src/conn.rs rename to src/client/conn.rs index 89c07d7..e774f79 100644 --- a/src/conn.rs +++ b/src/client/conn.rs @@ -6,8 +6,8 @@ use std::io::{BufferedReader, BufferedWriter, IoResult, TcpStream}; use std::sync::{Mutex, MutexGuard}; #[cfg(feature = "encode")] use encoding::{DecoderTrap, EncoderTrap, Encoding}; #[cfg(feature = "encode")] use encoding::label::encoding_from_whatwg_label; -use data::kinds::{IrcReader, IrcWriter}; -use data::message::ToMessage; +use client::data::kinds::{IrcReader, IrcWriter}; +use client::data::message::ToMessage; #[cfg(feature = "ssl")] use openssl::ssl::{SslContext, SslMethod, SslStream}; #[cfg(feature = "ssl")] use openssl::ssl::error::SslError; @@ -241,7 +241,7 @@ mod test { use super::Connection; use std::io::{MemReader, MemWriter}; use std::io::util::{NullReader, NullWriter}; - use data::message::Message; + use client::data::message::Message; #[cfg(feature = "encode")] use encoding::{DecoderTrap, Encoding}; #[cfg(feature = "encode")] use encoding::all::{ISO_8859_15, UTF_8}; diff --git a/src/data/command.rs b/src/client/data/command.rs similarity index 99% rename from src/data/command.rs rename to src/client/data/command.rs index efe90d7..e8bdbd5 100644 --- a/src/data/command.rs +++ b/src/client/data/command.rs @@ -2,7 +2,7 @@ #![stable] use std::io::{InvalidInput, IoError, IoResult}; use std::str::FromStr; -use data::message::{Message, ToMessage}; +use client::data::message::{Message, ToMessage}; /// List of all client commands as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812). This /// also includes commands from the diff --git a/src/data/config.rs b/src/client/data/config.rs similarity index 100% rename from src/data/config.rs rename to src/client/data/config.rs diff --git a/src/data/message.rs b/src/client/data/message.rs similarity index 100% rename from src/data/message.rs rename to src/client/data/message.rs diff --git a/src/data/mod.rs b/src/client/data/mod.rs similarity index 80% rename from src/data/mod.rs rename to src/client/data/mod.rs index 8688316..08dabe2 100644 --- a/src/data/mod.rs +++ b/src/client/data/mod.rs @@ -1,11 +1,11 @@ //! Data related to IRC functionality. #![stable] -pub use data::command::Command; -pub use data::config::Config; -pub use data::message::{Message, ToMessage}; -pub use data::response::Response; -pub use data::user::{AccessLevel, User}; +pub use client::data::command::Command; +pub use client::data::config::Config; +pub use client::data::message::{Message, ToMessage}; +pub use client::data::response::Response; +pub use client::data::user::{AccessLevel, User}; pub mod kinds { //! Trait definitions of appropriate Writers and Buffers for use with this library. diff --git a/src/data/response.rs b/src/client/data/response.rs similarity index 99% rename from src/data/response.rs rename to src/client/data/response.rs index 52dcc9f..2ced24a 100644 --- a/src/data/response.rs +++ b/src/client/data/response.rs @@ -3,7 +3,7 @@ #![allow(non_camel_case_types)] use std::num::FromPrimitive; use std::str::FromStr; -use data::message::Message; +use client::data::message::Message; /// List of all server responses as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812). /// All commands are documented with their expected form from the RFC. @@ -320,7 +320,7 @@ impl FromStr for Response { #[cfg(test)] mod test { use super::Response; - use data::message::ToMessage; + use client::data::message::ToMessage; #[test] fn from_message() { diff --git a/src/data/user.rs b/src/client/data/user.rs similarity index 99% rename from src/data/user.rs rename to src/client/data/user.rs index ffd2841..43607c0 100644 --- a/src/data/user.rs +++ b/src/client/data/user.rs @@ -206,7 +206,7 @@ impl<'a> Iterator for AccessLevelIterator<'a> { #[cfg(test)] mod test { use super::{AccessLevel, User}; - use super::AccessLevel::{Admin, HalfOp, Member, Oper, Owner, Voice}; + use super::AccessLevel::*; #[test] fn parse_access_level() { diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..cb4c537 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,6 @@ +//! A simple, thread-safe IRC client library. +#![stable] + +pub mod conn; +pub mod data; +pub mod server; diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs new file mode 100644 index 0000000..b553ef3 --- /dev/null +++ b/src/client/server/mod.rs @@ -0,0 +1,542 @@ +//! Interface for working with IRC Servers +#![stable] +use std::borrow::ToOwned; +use std::collections::HashMap; +use std::io::{BufferedReader, BufferedWriter, IoError, IoErrorKind, IoResult}; +use std::sync::{Mutex, RwLock}; +use client::conn::{Connection, NetStream}; +use client::data::{Command, Config, Message, Response, User}; +use client::data::Command::{JOIN, NICK, NICKSERV, PONG}; +use client::data::kinds::{IrcReader, IrcWriter}; +#[cfg(feature = "ctcp")] use time::now; + +pub mod utils; + +/// Trait describing core Server functionality. +#[stable] +pub trait Server<'a, T, U> { + /// Gets the configuration being used with this Server. + fn config(&self) -> &Config; + /// Sends a Command to this Server. + fn send(&self, _: Command) -> IoResult<()>; + /// Gets an Iterator over Messages received by this Server. + fn iter(&'a self) -> ServerIterator<'a, T, U>; + /// Gets a list of Users in the specified channel. + fn list_users(&self, _: &str) -> Option>; +} + +/// A thread-safe implementation of an IRC Server connection. +#[stable] +pub struct IrcServer { + /// The thread-safe IRC connection. + conn: Connection, + /// The configuration used with this connection. + config: Config, + /// A thread-safe map of channels to the list of users in them. + chanlists: Mutex>>, + /// A thread-safe index to track the current alternative nickname being used. + alt_nick_index: RwLock, +} + +/// An IrcServer over a buffered NetStream. +pub type NetIrcServer = IrcServer, BufferedWriter>; + +impl IrcServer, BufferedWriter> { + /// Creates a new IRC Server connection from the configuration at the specified path, + /// connecting immediately. + #[stable] + pub fn new(config: &str) -> IoResult { + IrcServer::from_config(try!(Config::load_utf8(config))) + } + + /// Creates a new IRC server connection from the specified configuration, connecting + /// immediately. + #[stable] + pub fn from_config(config: Config) -> IoResult { + let conn = try!(if config.use_ssl() { + Connection::connect_ssl(config.server(), config.port()) + } else { + Connection::connect(config.server(), config.port()) + }); + Ok(IrcServer { config: config, conn: conn, chanlists: Mutex::new(HashMap::new()), + alt_nick_index: RwLock::new(0) }) + } + + /// Reconnects to the IRC server. + #[unstable = "Feature is relatively new."] + pub fn reconnect(&self) -> IoResult<()> { + self.conn.reconnect(self.config().server(), self.config.port()) + } +} + +impl<'a, T: IrcReader, U: IrcWriter> Server<'a, T, U> for IrcServer { + fn config(&self) -> &Config { + &self.config + } + + #[cfg(feature = "encode")] + fn send(&self, cmd: Command) -> IoResult<()> { + self.conn.send(cmd, self.config.encoding()) + } + + #[cfg(not(feature = "encode"))] + fn send(&self, cmd: Command) -> IoResult<()> { + self.conn.send(cmd) + } + + fn iter(&'a self) -> ServerIterator<'a, T, U> { + ServerIterator::new(self) + } + + fn list_users(&self, chan: &str) -> Option> { + self.chanlists.lock().unwrap().get(&chan.to_owned()).cloned() + } +} + +impl IrcServer { + /// Creates an IRC server from the specified configuration, and any arbitrary Connection. + #[stable] + pub fn from_connection(config: Config, conn: Connection) -> IrcServer { + IrcServer { conn: conn, config: config, chanlists: Mutex::new(HashMap::new()), + alt_nick_index: RwLock::new(0) } + } + + /// Gets a reference to the IRC server's connection. + #[stable] + pub fn conn(&self) -> &Connection { + &self.conn + } + + /// Handles messages internally for basic bot functionality. + fn handle_message(&self, msg: &Message) { + if let Some(resp) = Response::from_message(msg) { + if resp == Response::RPL_NAMREPLY { + if cfg!(not(feature = "nochanlists")) { + if let Some(users) = msg.suffix.clone() { + if let [_, _, ref chan] = &msg.args[] { + for user in users.split_str(" ") { + if match self.chanlists.lock().unwrap().get_mut(chan) { + Some(vec) => { vec.push(User::new(user)); false }, + None => true, + } { + self.chanlists.lock().unwrap().insert(chan.clone(), + vec!(User::new(user))); + } + } + } + } + } + } else if resp == Response::RPL_ENDOFMOTD || resp == Response::ERR_NOMOTD { + if self.config.nick_password() != "" { + self.send(NICKSERV( + &format!("IDENTIFY {}", self.config.nick_password())[] + )).unwrap(); + } + for chan in self.config.channels().into_iter() { + self.send(JOIN(&chan[], None)).unwrap(); + } + } else if resp == Response::ERR_NICKNAMEINUSE || + resp == Response::ERR_ERRONEOUSNICKNAME { + let alt_nicks = self.config.get_alternate_nicknames(); + let mut index = self.alt_nick_index.write().unwrap(); + if *index >= alt_nicks.len() { + panic!("All specified nicknames were in use.") + } else { + self.send(NICK(alt_nicks[*index])).unwrap(); + *index += 1; + } + } + return + } + if &msg.command[] == "PING" { + self.send(PONG(&msg.suffix.as_ref().unwrap()[], None)).unwrap(); + } else if &msg.command[] == "JOIN" || &msg.command[] == "PART" { + let chan = match msg.suffix { + Some(ref suffix) => &suffix[], + None => &msg.args[0][], + }; + if cfg!(not(feature = "nochanlists")) { + if let Some(vec) = self.chanlists.lock().unwrap().get_mut(&String::from_str(chan)) { + if let Some(ref src) = msg.prefix { + if let Some(i) = src.find('!') { + if &msg.command[] == "JOIN" { + vec.push(User::new(&src[..i])); + } else { + if let Some(n) = vec.as_slice().position_elem(&User::new(&src[..i])) { + vec.swap_remove(n); + } + } + } + } + } + } + } else if let ("MODE", [ref chan, ref mode, ref user]) = (&msg.command[], &msg.args[]) { + if cfg!(not(feature = "nochanlists")) { + if let Some(vec) = self.chanlists.lock().unwrap().get_mut(chan) { + if let Some(n) = vec.as_slice().position_elem(&User::new(&user[])) { + vec[n].update_access_level(&mode[]); + } + } + } + } else { + self.handle_ctcp(msg); + } + } + + /// Handles CTCP requests if the CTCP feature is enabled. + #[cfg(feature = "ctcp")] + fn handle_ctcp(&self, msg: &Message) { + let source = match msg.prefix { + Some(ref source) => source.find('!').map_or(&source[], |i| &source[..i]), + None => "", + }; + if let ("PRIVMSG", [ref target]) = (&msg.command[], &msg.args[]) { + let resp = if target.starts_with("#") { &target[] } else { source }; + match msg.suffix { + Some(ref msg) if msg.starts_with("\u{001}") => { + let tokens: Vec<_> = { + let end = if msg.ends_with("\u{001}") { + msg.len() - 1 + } else { + msg.len() + }; + msg[1..end].split_str(" ").collect() + }; + match tokens[0] { + "FINGER" => self.send_ctcp(resp, &format!("FINGER :{} ({})", + self.config.real_name(), + self.config.username())[]), + "VERSION" => self.send_ctcp(resp, "VERSION irc:git:Rust"), + "SOURCE" => { + self.send_ctcp(resp, "SOURCE https://github.com/aatxe/irc"); + self.send_ctcp(resp, "SOURCE"); + }, + "PING" => self.send_ctcp(resp, &format!("PING {}", tokens[1])[]), + "TIME" => self.send_ctcp(resp, &format!("TIME :{}", now().rfc822z())[]), + "USERINFO" => self.send_ctcp(resp, &format!("USERINFO :{}", + self.config.user_info())[]), + _ => {} + } + }, + _ => {} + } + } + } + + /// Sends a CTCP-escaped message. + #[cfg(feature = "ctcp")] + fn send_ctcp(&self, target: &str, msg: &str) { + self.send(Command::NOTICE(target, &format!("\u{001}{}\u{001}", msg)[])).unwrap(); + } + + /// Handles CTCP requests if the CTCP feature is enabled. + #[cfg(not(feature = "ctcp"))] fn handle_ctcp(&self, _: &Message) {} +} + +/// An Iterator over an IrcServer's incoming Messages. +#[stable] +pub struct ServerIterator<'a, T: IrcReader, U: IrcWriter> { + server: &'a IrcServer +} + +impl<'a, T: IrcReader, U: IrcWriter> ServerIterator<'a, T, U> { + /// Creates a new ServerIterator for the desired IrcServer. + #[experimental = "Design will change to accomodate new behavior."] + pub fn new(server: &IrcServer) -> ServerIterator { + ServerIterator { server: server } + } + + /// Gets the next line from the connection. + #[cfg(feature = "encode")] + fn get_next_line(&self) -> IoResult { + self.server.conn.recv(self.server.config.encoding()) + } + + /// Gets the next line from the connection. + #[cfg(not(feature = "encode"))] + fn get_next_line(&self) -> IoResult { + self.server.conn.recv() + } +} + +impl<'a, T: IrcReader, U: IrcWriter> Iterator for ServerIterator<'a, T, U> { + type Item = IoResult; + fn next(&mut self) -> Option> { + let res = self.get_next_line().and_then(|msg| + match msg.parse() { + Some(msg) => { + self.server.handle_message(&msg); + Ok(msg) + }, + None => Err(IoError { + kind: IoErrorKind::InvalidInput, + desc: "Failed to parse message.", + detail: Some(msg) + }) + } + ); + match res { + Err(ref err) if err.kind == IoErrorKind::EndOfFile => None, + _ => Some(res) + } + } +} + +#[cfg(test)] +mod test { + use super::{IrcServer, Server}; + use std::default::Default; + use std::io::{MemReader, MemWriter}; + use std::io::util::{NullReader, NullWriter}; + use client::conn::Connection; + use client::data::{Config, User}; + use client::data::command::Command::PRIVMSG; + use client::data::kinds::IrcReader; + + pub fn test_config() -> Config { + Config { + owners: Some(vec![format!("test")]), + nickname: Some(format!("test")), + alt_nicks: Some(vec![format!("test2")]), + server: Some(format!("irc.test.net")), + channels: Some(vec![format!("#test"), format!("#test2")]), + user_info: Some(format!("Testing.")), + .. Default::default() + } + } + + pub fn get_server_value(server: IrcServer) -> String { + String::from_utf8((*server.conn().writer()).get_ref().to_vec()).unwrap() + } + + #[test] + fn iterator() { + let exp = "PRIVMSG test :Hi!\r\nPRIVMSG test :This is a test!\r\n\ + :test!test@test JOIN #test\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(exp.as_bytes().to_vec()), NullWriter + )); + let mut messages = String::new(); + for message in server.iter() { + messages.push_str(&message.unwrap().into_string()[]); + } + assert_eq!(&messages[], exp); + } + + #[test] + fn handle_message() { + let value = "PING :irc.test.net\r\n:irc.test.net 376 test :End of /MOTD command.\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(&get_server_value(server)[], + "PONG :irc.test.net\r\nJOIN #test\r\nJOIN #test2\r\n"); + } + + #[test] + fn handle_end_motd_with_nick_password() { + let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; + let server = IrcServer::from_connection(Config { + nick_password: Some(format!("password")), + channels: Some(vec![format!("#test"), format!("#test2")]), + .. Default::default() + }, Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(&get_server_value(server)[], + "NICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n"); + } + + #[test] + fn nickname_in_use() { + let value = ":irc.pdgn.co 433 * test :Nickname is already in use."; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(&get_server_value(server)[], "NICK :test2\r\n"); + } + + #[test] + #[should_fail(message = "All specified nicknames were in use.")] + fn ran_out_of_nicknames() { + let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\ + :irc.pdgn.co 433 * test2 :Nickname is already in use.\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + } + + #[test] + fn send() { + let server = IrcServer::from_connection(test_config(), Connection::new( + NullReader, MemWriter::new() + )); + assert!(server.send(PRIVMSG("#test", "Hi there!")).is_ok()); + assert_eq!(&get_server_value(server)[], "PRIVMSG #test :Hi there!\r\n"); + } + + #[cfg(not(feature = "nochanlists"))] + #[test] + fn user_tracking_names() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), NullWriter + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(server.list_users("#test").unwrap(), + vec![User::new("test"), User::new("~owner"), User::new("&admin")]) + } + + #[cfg(not(feature = "nochanlists"))] + #[test] + fn user_tracking_names_join() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ + :test2!test@test JOIN #test\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), NullWriter + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(server.list_users("#test").unwrap(), + vec![User::new("test"), User::new("~owner"), User::new("&admin"), User::new("test2")]) + } + + #[cfg(not(feature = "nochanlists"))] + #[test] + fn user_tracking_names_part() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ + :owner!test@test PART #test\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), NullWriter + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(server.list_users("#test").unwrap(), + vec![User::new("test"), User::new("&admin")]) + } + + #[cfg(not(feature = "nochanlists"))] + #[test] + fn user_tracking_names_mode() { + let value = ":irc.test.net 353 test = #test :+test ~owner &admin\r\n\ + :test!test@test MODE #test +o test\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), NullWriter + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(server.list_users("#test").unwrap(), + vec![User::new("@test"), User::new("~owner"), User::new("&admin")]); + let mut exp = User::new("@test"); + exp.update_access_level("+v"); + assert_eq!(server.list_users("#test").unwrap()[0].highest_access_level(), + exp.highest_access_level()); + // The following tests if the maintained user contains the same entries as what is expected + // but ignores the ordering of these entries. + let mut levels = server.list_users("#test").unwrap()[0].access_levels(); + levels.retain(|l| exp.access_levels().contains(l)); + assert_eq!(levels.len(), exp.access_levels().len()); + } + + #[test] + #[cfg(feature = "ctcp")] + fn finger_response() { + let value = ":test!test@test PRIVMSG test :\u{001}FINGER\u{001}\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(&get_server_value(server)[], "NOTICE test :\u{001}FINGER :test (test)\u{001}\ + \r\n"); + } + + #[test] + #[cfg(feature = "ctcp")] + fn version_response() { + let value = ":test!test@test PRIVMSG test :\u{001}VERSION\u{001}\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(&get_server_value(server)[], "NOTICE test :\u{001}VERSION irc:git:Rust\u{001}\ + \r\n"); + } + + #[test] + #[cfg(feature = "ctcp")] + fn source_response() { + let value = ":test!test@test PRIVMSG test :\u{001}SOURCE\u{001}\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(&get_server_value(server)[], + "NOTICE test :\u{001}SOURCE https://github.com/aatxe/irc\u{001}\r\n\ + NOTICE test :\u{001}SOURCE\u{001}\r\n"); + } + + #[test] + #[cfg(feature = "ctcp")] + fn ctcp_ping_response() { + let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(&get_server_value(server)[], "NOTICE test :\u{001}PING test\u{001}\r\n"); + } + + #[test] + #[cfg(feature = "ctcp")] + fn time_response() { + let value = ":test!test@test PRIVMSG test :\u{001}TIME\u{001}\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + let val = get_server_value(server); + assert!(val.starts_with("NOTICE test :\u{001}TIME :")); + assert!(val.ends_with("\u{001}\r\n")); + } + + #[test] + #[cfg(feature = "ctcp")] + fn user_info_response() { + let value = ":test!test@test PRIVMSG test :\u{001}USERINFO\u{001}\r\n"; + let server = IrcServer::from_connection(test_config(), Connection::new( + MemReader::new(value.as_bytes().to_vec()), MemWriter::new() + )); + for message in server.iter() { + println!("{:?}", message); + } + assert_eq!(&get_server_value(server)[], "NOTICE test :\u{001}USERINFO :Testing.\u{001}\ + \r\n"); + } +} diff --git a/src/server/utils.rs b/src/client/server/utils.rs similarity index 97% rename from src/server/utils.rs rename to src/client/server/utils.rs index 6927b4f..c213af8 100644 --- a/src/server/utils.rs +++ b/src/client/server/utils.rs @@ -2,13 +2,13 @@ #![stable] use std::io::IoResult; -use data::{Command, Config, User}; -use data::Command::{CAP, INVITE, JOIN, KICK, KILL, MODE, NICK, NOTICE}; -use data::Command::{OPER, PASS, PONG, PRIVMSG, QUIT, SAMODE, SANICK, TOPIC, USER}; -use data::command::CapSubCommand::{END, REQ}; -use data::kinds::{IrcReader, IrcWriter}; +use client::data::{Command, Config, User}; +use client::data::Command::{CAP, INVITE, JOIN, KICK, KILL, MODE, NICK, NOTICE}; +use client::data::Command::{OPER, PASS, PONG, PRIVMSG, QUIT, SAMODE, SANICK, TOPIC, USER}; +use client::data::command::CapSubCommand::{END, REQ}; +use client::data::kinds::{IrcReader, IrcWriter}; #[cfg(feature = "ctcp")] use time::get_time; -use server::{Server, ServerIterator}; +use client::server::{Server, ServerIterator}; /// Functionality-providing wrapper for Server. /// Wrappers are currently not thread-safe, and should be created per-thread, as needed. @@ -238,10 +238,10 @@ mod test { use std::default::Default; use std::io::MemWriter; use std::io::util::NullReader; - use conn::Connection; - use data::Config; - use server::IrcServer; - use server::test::{get_server_value, test_config}; + use client::conn::Connection; + use client::data::Config; + use client::server::IrcServer; + use client::server::test::{get_server_value, test_config}; #[test] fn identify() { diff --git a/src/lib.rs b/src/lib.rs index 550a49a..49db07c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! A simple, thread-safe IRC client library. +//! A simple, thread-safe IRC library. #![crate_name = "irc"] #![crate_type = "lib"] #![unstable] @@ -11,6 +11,5 @@ extern crate "rustc-serialize" as rustc_serialize; #[cfg(feature = "ssl")] extern crate openssl; -pub mod conn; -pub mod data; +pub mod client; pub mod server; diff --git a/src/server/mod.rs b/src/server/mod.rs index c269949..56b1e92 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,542 +1,2 @@ -//! Interface for working with IRC Servers -#![stable] -use std::borrow::ToOwned; -use std::collections::HashMap; -use std::io::{BufferedReader, BufferedWriter, IoError, IoErrorKind, IoResult}; -use std::sync::{Mutex, RwLock}; -use conn::{Connection, NetStream}; -use data::{Command, Config, Message, Response, User}; -use data::Command::{JOIN, NICK, NICKSERV, PONG}; -use data::kinds::{IrcReader, IrcWriter}; -#[cfg(feature = "ctcp")] use time::now; - -pub mod utils; - -/// Trait describing core Server functionality. -#[stable] -pub trait Server<'a, T, U> { - /// Gets the configuration being used with this Server. - fn config(&self) -> &Config; - /// Sends a Command to this Server. - fn send(&self, _: Command) -> IoResult<()>; - /// Gets an Iterator over Messages received by this Server. - fn iter(&'a self) -> ServerIterator<'a, T, U>; - /// Gets a list of Users in the specified channel. - fn list_users(&self, _: &str) -> Option>; -} - -/// A thread-safe implementation of an IRC Server connection. -#[stable] -pub struct IrcServer { - /// The thread-safe IRC connection. - conn: Connection, - /// The configuration used with this connection. - config: Config, - /// A thread-safe map of channels to the list of users in them. - chanlists: Mutex>>, - /// A thread-safe index to track the current alternative nickname being used. - alt_nick_index: RwLock, -} - -/// An IrcServer over a buffered NetStream. -pub type NetIrcServer = IrcServer, BufferedWriter>; - -impl IrcServer, BufferedWriter> { - /// Creates a new IRC Server connection from the configuration at the specified path, - /// connecting immediately. - #[stable] - pub fn new(config: &str) -> IoResult { - IrcServer::from_config(try!(Config::load_utf8(config))) - } - - /// Creates a new IRC server connection from the specified configuration, connecting - /// immediately. - #[stable] - pub fn from_config(config: Config) -> IoResult { - let conn = try!(if config.use_ssl() { - Connection::connect_ssl(config.server(), config.port()) - } else { - Connection::connect(config.server(), config.port()) - }); - Ok(IrcServer { config: config, conn: conn, chanlists: Mutex::new(HashMap::new()), - alt_nick_index: RwLock::new(0) }) - } - - /// Reconnects to the IRC server. - #[unstable = "Feature is relatively new."] - pub fn reconnect(&self) -> IoResult<()> { - self.conn.reconnect(self.config().server(), self.config.port()) - } -} - -impl<'a, T: IrcReader, U: IrcWriter> Server<'a, T, U> for IrcServer { - fn config(&self) -> &Config { - &self.config - } - - #[cfg(feature = "encode")] - fn send(&self, cmd: Command) -> IoResult<()> { - self.conn.send(cmd, self.config.encoding()) - } - - #[cfg(not(feature = "encode"))] - fn send(&self, cmd: Command) -> IoResult<()> { - self.conn.send(cmd) - } - - fn iter(&'a self) -> ServerIterator<'a, T, U> { - ServerIterator::new(self) - } - - fn list_users(&self, chan: &str) -> Option> { - self.chanlists.lock().unwrap().get(&chan.to_owned()).cloned() - } -} - -impl IrcServer { - /// Creates an IRC server from the specified configuration, and any arbitrary Connection. - #[stable] - pub fn from_connection(config: Config, conn: Connection) -> IrcServer { - IrcServer { conn: conn, config: config, chanlists: Mutex::new(HashMap::new()), - alt_nick_index: RwLock::new(0) } - } - - /// Gets a reference to the IRC server's connection. - #[stable] - pub fn conn(&self) -> &Connection { - &self.conn - } - - /// Handles messages internally for basic bot functionality. - fn handle_message(&self, msg: &Message) { - if let Some(resp) = Response::from_message(msg) { - if resp == Response::RPL_NAMREPLY { - if cfg!(not(feature = "nochanlists")) { - if let Some(users) = msg.suffix.clone() { - if let [_, _, ref chan] = &msg.args[] { - for user in users.split_str(" ") { - if match self.chanlists.lock().unwrap().get_mut(chan) { - Some(vec) => { vec.push(User::new(user)); false }, - None => true, - } { - self.chanlists.lock().unwrap().insert(chan.clone(), - vec!(User::new(user))); - } - } - } - } - } - } else if resp == Response::RPL_ENDOFMOTD || resp == Response::ERR_NOMOTD { - if self.config.nick_password() != "" { - self.send(NICKSERV( - &format!("IDENTIFY {}", self.config.nick_password())[] - )).unwrap(); - } - for chan in self.config.channels().into_iter() { - self.send(JOIN(&chan[], None)).unwrap(); - } - } else if resp == Response::ERR_NICKNAMEINUSE || - resp == Response::ERR_ERRONEOUSNICKNAME { - let alt_nicks = self.config.get_alternate_nicknames(); - let mut index = self.alt_nick_index.write().unwrap(); - if *index >= alt_nicks.len() { - panic!("All specified nicknames were in use.") - } else { - self.send(NICK(alt_nicks[*index])).unwrap(); - *index += 1; - } - } - return - } - if &msg.command[] == "PING" { - self.send(PONG(&msg.suffix.as_ref().unwrap()[], None)).unwrap(); - } else if &msg.command[] == "JOIN" || &msg.command[] == "PART" { - let chan = match msg.suffix { - Some(ref suffix) => &suffix[], - None => &msg.args[0][], - }; - if cfg!(not(feature = "nochanlists")) { - if let Some(vec) = self.chanlists.lock().unwrap().get_mut(&String::from_str(chan)) { - if let Some(ref src) = msg.prefix { - if let Some(i) = src.find('!') { - if &msg.command[] == "JOIN" { - vec.push(User::new(&src[..i])); - } else { - if let Some(n) = vec.as_slice().position_elem(&User::new(&src[..i])) { - vec.swap_remove(n); - } - } - } - } - } - } - } else if let ("MODE", [ref chan, ref mode, ref user]) = (&msg.command[], &msg.args[]) { - if cfg!(not(feature = "nochanlists")) { - if let Some(vec) = self.chanlists.lock().unwrap().get_mut(chan) { - if let Some(n) = vec.as_slice().position_elem(&User::new(&user[])) { - vec[n].update_access_level(&mode[]); - } - } - } - } else { - self.handle_ctcp(msg); - } - } - - /// Handles CTCP requests if the CTCP feature is enabled. - #[cfg(feature = "ctcp")] - fn handle_ctcp(&self, msg: &Message) { - let source = match msg.prefix { - Some(ref source) => source.find('!').map_or(&source[], |i| &source[..i]), - None => "", - }; - if let ("PRIVMSG", [ref target]) = (&msg.command[], &msg.args[]) { - let resp = if target.starts_with("#") { &target[] } else { source }; - match msg.suffix { - Some(ref msg) if msg.starts_with("\u{001}") => { - let tokens: Vec<_> = { - let end = if msg.ends_with("\u{001}") { - msg.len() - 1 - } else { - msg.len() - }; - msg[1..end].split_str(" ").collect() - }; - match tokens[0] { - "FINGER" => self.send_ctcp(resp, &format!("FINGER :{} ({})", - self.config.real_name(), - self.config.username())[]), - "VERSION" => self.send_ctcp(resp, "VERSION irc:git:Rust"), - "SOURCE" => { - self.send_ctcp(resp, "SOURCE https://github.com/aatxe/irc"); - self.send_ctcp(resp, "SOURCE"); - }, - "PING" => self.send_ctcp(resp, &format!("PING {}", tokens[1])[]), - "TIME" => self.send_ctcp(resp, &format!("TIME :{}", now().rfc822z())[]), - "USERINFO" => self.send_ctcp(resp, &format!("USERINFO :{}", - self.config.user_info())[]), - _ => {} - } - }, - _ => {} - } - } - } - - /// Sends a CTCP-escaped message. - #[cfg(feature = "ctcp")] - fn send_ctcp(&self, target: &str, msg: &str) { - self.send(Command::NOTICE(target, &format!("\u{001}{}\u{001}", msg)[])).unwrap(); - } - - /// Handles CTCP requests if the CTCP feature is enabled. - #[cfg(not(feature = "ctcp"))] fn handle_ctcp(&self, _: &Message) {} -} - -/// An Iterator over an IrcServer's incoming Messages. -#[stable] -pub struct ServerIterator<'a, T: IrcReader, U: IrcWriter> { - server: &'a IrcServer -} - -impl<'a, T: IrcReader, U: IrcWriter> ServerIterator<'a, T, U> { - /// Creates a new ServerIterator for the desired IrcServer. - #[experimental = "Design will change to accomodate new behavior."] - pub fn new(server: &IrcServer) -> ServerIterator { - ServerIterator { server: server } - } - - /// Gets the next line from the connection. - #[cfg(feature = "encode")] - fn get_next_line(&self) -> IoResult { - self.server.conn.recv(self.server.config.encoding()) - } - - /// Gets the next line from the connection. - #[cfg(not(feature = "encode"))] - fn get_next_line(&self) -> IoResult { - self.server.conn.recv() - } -} - -impl<'a, T: IrcReader, U: IrcWriter> Iterator for ServerIterator<'a, T, U> { - type Item = IoResult; - fn next(&mut self) -> Option> { - let res = self.get_next_line().and_then(|msg| - match msg.parse() { - Some(msg) => { - self.server.handle_message(&msg); - Ok(msg) - }, - None => Err(IoError { - kind: IoErrorKind::InvalidInput, - desc: "Failed to parse message.", - detail: Some(msg) - }) - } - ); - match res { - Err(ref err) if err.kind == IoErrorKind::EndOfFile => None, - _ => Some(res) - } - } -} - -#[cfg(test)] -mod test { - use super::{IrcServer, Server}; - use std::default::Default; - use std::io::{MemReader, MemWriter}; - use std::io::util::{NullReader, NullWriter}; - use conn::Connection; - use data::{Config, User}; - use data::command::Command::PRIVMSG; - use data::kinds::IrcReader; - - pub fn test_config() -> Config { - Config { - owners: Some(vec![format!("test")]), - nickname: Some(format!("test")), - alt_nicks: Some(vec![format!("test2")]), - server: Some(format!("irc.test.net")), - channels: Some(vec![format!("#test"), format!("#test2")]), - user_info: Some(format!("Testing.")), - .. Default::default() - } - } - - pub fn get_server_value(server: IrcServer) -> String { - String::from_utf8((*server.conn().writer()).get_ref().to_vec()).unwrap() - } - - #[test] - fn iterator() { - let exp = "PRIVMSG test :Hi!\r\nPRIVMSG test :This is a test!\r\n\ - :test!test@test JOIN #test\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(exp.as_bytes().to_vec()), NullWriter - )); - let mut messages = String::new(); - for message in server.iter() { - messages.push_str(&message.unwrap().into_string()[]); - } - assert_eq!(&messages[], exp); - } - - #[test] - fn handle_message() { - let value = "PING :irc.test.net\r\n:irc.test.net 376 test :End of /MOTD command.\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(&get_server_value(server)[], - "PONG :irc.test.net\r\nJOIN #test\r\nJOIN #test2\r\n"); - } - - #[test] - fn handle_end_motd_with_nick_password() { - let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; - let server = IrcServer::from_connection(Config { - nick_password: Some(format!("password")), - channels: Some(vec![format!("#test"), format!("#test2")]), - .. Default::default() - }, Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(&get_server_value(server)[], - "NICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n"); - } - - #[test] - fn nickname_in_use() { - let value = ":irc.pdgn.co 433 * test :Nickname is already in use."; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(&get_server_value(server)[], "NICK :test2\r\n"); - } - - #[test] - #[should_fail(message = "All specified nicknames were in use.")] - fn ran_out_of_nicknames() { - let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\ - :irc.pdgn.co 433 * test2 :Nickname is already in use.\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - } - - #[test] - fn send() { - let server = IrcServer::from_connection(test_config(), Connection::new( - NullReader, MemWriter::new() - )); - assert!(server.send(PRIVMSG("#test", "Hi there!")).is_ok()); - assert_eq!(&get_server_value(server)[], "PRIVMSG #test :Hi there!\r\n"); - } - - #[cfg(not(feature = "nochanlists"))] - #[test] - fn user_tracking_names() { - let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), NullWriter - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(server.list_users("#test").unwrap(), - vec![User::new("test"), User::new("~owner"), User::new("&admin")]) - } - - #[cfg(not(feature = "nochanlists"))] - #[test] - fn user_tracking_names_join() { - let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ - :test2!test@test JOIN #test\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), NullWriter - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(server.list_users("#test").unwrap(), - vec![User::new("test"), User::new("~owner"), User::new("&admin"), User::new("test2")]) - } - - #[cfg(not(feature = "nochanlists"))] - #[test] - fn user_tracking_names_part() { - let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ - :owner!test@test PART #test\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), NullWriter - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(server.list_users("#test").unwrap(), - vec![User::new("test"), User::new("&admin")]) - } - - #[cfg(not(feature = "nochanlists"))] - #[test] - fn user_tracking_names_mode() { - let value = ":irc.test.net 353 test = #test :+test ~owner &admin\r\n\ - :test!test@test MODE #test +o test\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), NullWriter - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(server.list_users("#test").unwrap(), - vec![User::new("@test"), User::new("~owner"), User::new("&admin")]); - let mut exp = User::new("@test"); - exp.update_access_level("+v"); - assert_eq!(server.list_users("#test").unwrap()[0].highest_access_level(), - exp.highest_access_level()); - // The following tests if the maintained user contains the same entries as what is expected - // but ignores the ordering of these entries. - let mut levels = server.list_users("#test").unwrap()[0].access_levels(); - levels.retain(|l| exp.access_levels().contains(l)); - assert_eq!(levels.len(), exp.access_levels().len()); - } - - #[test] - #[cfg(feature = "ctcp")] - fn finger_response() { - let value = ":test!test@test PRIVMSG test :\u{001}FINGER\u{001}\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(&get_server_value(server)[], "NOTICE test :\u{001}FINGER :test (test)\u{001}\ - \r\n"); - } - - #[test] - #[cfg(feature = "ctcp")] - fn version_response() { - let value = ":test!test@test PRIVMSG test :\u{001}VERSION\u{001}\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(&get_server_value(server)[], "NOTICE test :\u{001}VERSION irc:git:Rust\u{001}\ - \r\n"); - } - - #[test] - #[cfg(feature = "ctcp")] - fn source_response() { - let value = ":test!test@test PRIVMSG test :\u{001}SOURCE\u{001}\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(&get_server_value(server)[], - "NOTICE test :\u{001}SOURCE https://github.com/aatxe/irc\u{001}\r\n\ - NOTICE test :\u{001}SOURCE\u{001}\r\n"); - } - - #[test] - #[cfg(feature = "ctcp")] - fn ctcp_ping_response() { - let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(&get_server_value(server)[], "NOTICE test :\u{001}PING test\u{001}\r\n"); - } - - #[test] - #[cfg(feature = "ctcp")] - fn time_response() { - let value = ":test!test@test PRIVMSG test :\u{001}TIME\u{001}\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - let val = get_server_value(server); - assert!(val.starts_with("NOTICE test :\u{001}TIME :")); - assert!(val.ends_with("\u{001}\r\n")); - } - - #[test] - #[cfg(feature = "ctcp")] - fn user_info_response() { - let value = ":test!test@test PRIVMSG test :\u{001}USERINFO\u{001}\r\n"; - let server = IrcServer::from_connection(test_config(), Connection::new( - MemReader::new(value.as_bytes().to_vec()), MemWriter::new() - )); - for message in server.iter() { - println!("{:?}", message); - } - assert_eq!(&get_server_value(server)[], "NOTICE test :\u{001}USERINFO :Testing.\u{001}\ - \r\n"); - } -} +//! A simple, thread-safe IRC server library. +#![experimental]