//! Interface for working with IRC Servers #![experimental] use std::collections::HashMap; use std::io::{BufferedReader, BufferedWriter, IoResult}; use std::sync::{Mutex, RWLock}; use conn::{Connection, NetStream}; use data::{Command, Config, Message, Response, User}; use data::Command::{JOIN, NICK, PONG}; use data::kinds::{IrcReader, IrcWriter}; pub mod utils; /// Trait describing core Server functionality. #[experimental] 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. #[experimental] 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. #[experimental] pub fn new(config: &str) -> IoResult { IrcServer::from_config(try!(Config::load_utf8(config))) } /// Creates a new IRC server connection from the configuration at the specified path with the /// specified timeout in milliseconds, connecting immediately. #[experimental] pub fn with_timeout(config: &str, timeout_ms: u64) -> IoResult { IrcServer::from_config_with_timeout(try!(Config::load_utf8(config)), timeout_ms) } /// Creates a new IRC server connection from the specified configuration, connecting /// immediately. #[experimental] 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(0u) }) } /// Creates a new IRC server connection from the specified configuration with the specified /// timeout in milliseconds, connecting /// immediately. #[experimental] pub fn from_config_with_timeout(config: Config, timeout_ms: u64) -> IoResult { let conn = try!(if config.use_ssl() { Connection::connect_ssl_with_timeout(config.server(), config.port(), timeout_ms) } else { Connection::connect_with_timeout(config.server(), config.port(), timeout_ms) }); Ok(IrcServer { config: config, conn: conn, chanlists: Mutex::new(HashMap::new()), alt_nick_index: RWLock::new(0u) }) } } impl<'a, T: IrcReader, U: IrcWriter> Server<'a, T, U> for IrcServer { fn config(&self) -> &Config { &self.config } #[cfg(feature = "encode")] fn send(&self, command: Command) -> IoResult<()> { self.conn.send(command.to_message(), self.config.encoding()) } #[cfg(not(feature = "encode"))] fn send(&self, command: Command) -> IoResult<()> { self.conn.send(command.to_message()) } fn iter(&'a self) -> ServerIterator<'a, T, U> { ServerIterator::new(self) } fn list_users(&self, chan: &str) -> Option> { self.chanlists.lock().get(&chan.into_string()).cloned() } } impl IrcServer { /// Creates an IRC server from the specified configuration, and any arbitrary Connection. #[experimental] pub fn from_connection(config: Config, conn: Connection) -> IrcServer { IrcServer { conn: conn, config: config, chanlists: Mutex::new(HashMap::new()), alt_nick_index: RWLock::new(0u) } } /// Gets a reference to the IRC server's connection. pub fn conn(&self) -> &Connection { &self.conn } /// Handles messages internally for basic bot functionality. #[experimental] fn handle_message(&self, msg: &Message) { if let Some(resp) = Response::from_message(msg) { if resp == Response::RPL_NAMREPLY { if let Some(users) = msg.suffix.clone() { if let [_, _, ref chan] = msg.args[] { for user in users.split_str(" ") { if match self.chanlists.lock().get_mut(chan) { Some(vec) => { vec.push(User::new(user)); false }, None => true, } { self.chanlists.lock().insert(chan.clone(), vec!(User::new(user))); } } } } } else if resp == Response::RPL_ENDOFMOTD || resp == Response::ERR_NOMOTD { 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(); if *index.deref() >= alt_nicks.len() { panic!("All specified nicknames were in use.") } else { self.send(NICK(alt_nicks[*index.deref()])).unwrap(); *index.deref_mut() += 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 let Some(vec) = self.chanlists.lock().get_mut(&String::from_str(chan)) { if let Some(ref source) = msg.prefix { if let Some(i) = source.find('!') { if msg.command[] == "JOIN" { vec.push(User::new(source[..i])); } else { if let Some(n) = vec.as_slice().position_elem(&User::new(source[..i])) { vec.swap_remove(n); } } } } } } else if let ("MODE", [ref chan, ref mode, ref user]) = (msg.command[], msg.args[]) { if let Some(vec) = self.chanlists.lock().get_mut(chan) { if let Some(n) = vec.as_slice().position_elem(&User::new(user[])) { vec[n].update_access_level(mode[]); } } } } } /// An Iterator over an IrcServer's incoming Messages. #[experimental] 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] 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> { fn next(&mut self) -> Option { match self.get_next_line() { Err(_) => None, Ok(msg) => { let message = from_str(msg[]); self.server.handle_message(message.as_ref().unwrap()); message } } } } #[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")]), .. Default::default() } } pub fn get_server_value(server: IrcServer) -> String { String::from_utf8((*server.conn().writer().deref()).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.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 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"); } #[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")]) } #[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")]) } #[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")]) } #[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()); } }