diff --git a/.travis.yml b/.travis.yml index 6eae4d7..b284ea8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: rust script: - - chmod +x mktestconfig.sh - - ./mktestconfig.sh - cargo build --verbose - cargo test --verbose - cargo doc --verbose diff --git a/Cargo.toml b/Cargo.toml index 83f1052..c538663 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,3 @@ name = "irc" version = "0.0.1" authors = ["Aaron Weiss "] - -[[example]] -name = "simple" diff --git a/examples/simple.rs b/examples/simple.rs index 566a78c..9cc950a 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -2,9 +2,9 @@ extern crate irc; use std::collections::HashMap; -use irc::Server; -use irc::bot::IrcServer; -use irc::data::{Config, Message}; +use irc::data::config::Config; +use irc::server::{IrcServer, Server}; +use irc::server::utils::identify; fn main() { let config = Config { @@ -18,10 +18,9 @@ fn main() { channels: vec!("#vana".into_string()), options: HashMap::new(), }; - let mut server = IrcServer::new_with_config(config).unwrap(); - server.send(Message::new(None, "NICK", vec!["pickles"], None)).unwrap(); - server.send(Message::new(None, "USER", vec!["pickles", "0", "*", "pickles"], None)).unwrap(); - for message in server { - println!("RCV: {}", message); + let server = IrcServer::from_config(config).unwrap(); + identify(&server).unwrap(); + for message in server.iter() { + print!("{}", message.into_string()); } } diff --git a/mktestconfig.sh b/mktestconfig.sh deleted file mode 100644 index c650ad8..0000000 --- a/mktestconfig.sh +++ /dev/null @@ -1 +0,0 @@ -echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.fyrechat.net\",\"port\": 6667,\"channels\": [\"#test\", \"#test2\"],\"options\": {}}" > config.json diff --git a/src/bot.rs b/src/bot.rs deleted file mode 100644 index 4e1e015..0000000 --- a/src/bot.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::io::{BufferedReader, BufferedWriter, IoResult, TcpStream}; -use {Server, process}; -use conn::Connection; -use data::{Config, IrcReader, IrcWriter, Message, User}; - -pub struct IrcServer<'a, T, U> where T: IrcWriter, U: IrcReader { - pub conn: Connection, - pub config: Config, - chanlists: RefCell>>, -} - -impl<'a> IrcServer<'a, BufferedWriter, BufferedReader> { - pub fn new() -> IoResult, BufferedReader>> { - let config = try!(Config::load_utf8("config.json")); - let conn = try!(Connection::connect(config.server[], config.port)); - Ok(IrcServer { - conn: conn, - config: config, - chanlists: RefCell::new(HashMap::new()), - }) - } - - pub fn new_with_config(config: Config) -> IoResult, BufferedReader>> { - let conn = try!(Connection::connect(config.server[], config.port)); - Ok(IrcServer { - conn: conn, - config: config, - chanlists: RefCell::new(HashMap::new()), - }) - } -} - -impl<'a, T, U> Iterator for IrcServer<'a, T, U> where T: IrcWriter, U: IrcReader { - fn next(&mut self) -> Option { - let line_res = self.conn.reader().read_line(); - if let Err(e) = line_res { println!("{}", e); return None; } - let line = line_res.unwrap(); - let processed = process(line[]); - if let Err(e) = processed { println!("{}", e); return None; } - let (source, command, args) = processed.unwrap(); - Some(Message::new(Some(source), command, args, None)) - } -} - -impl<'a, T, U> Server<'a> for IrcServer<'a, T, U> where T: IrcWriter, U: IrcReader { - fn send(&self, message: Message) -> IoResult<()> { - self.conn.send(message) - } - - fn config(&self) -> &Config { - &self.config - } - - fn get_users(&self, chan: &str) -> Option> { - self.chanlists.borrow_mut().find_copy(&chan.into_string()) - } -} - -impl<'a, T, U> IrcServer<'a, T, U> where T: IrcWriter, U: IrcReader { - pub fn from_connection(conn: Connection) -> IoResult> { - Ok(IrcServer { - conn: conn, - config: try!(Config::load_utf8("config.json")), - chanlists: RefCell::new(HashMap::new()), - }) - } -} diff --git a/src/conn.rs b/src/conn.rs index 011b4d5..b057c50 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -1,51 +1,47 @@ -use std::cell::{RefCell, RefMut}; -use std::io::{BufferedReader, BufferedWriter, IoResult, TcpStream, Writer}; -use data::{IrcReader, IrcWriter, Message}; +//! Thread-safe connections on any IrcWriters and IrcReaders +#![experimental] +use std::sync::Mutex; +use std::io::{BufferedReader, BufferedWriter, IoResult, TcpStream}; +use data::kinds::{IrcWriter, IrcReader}; +use data::message::Message; +/// A thread-safe connection +#[experimental] pub struct Connection where T: IrcWriter, U: IrcReader { - writer: RefCell, - reader: RefCell, + writer: Mutex, + reader: Mutex, } impl Connection, BufferedReader> { + /// Creates a thread-safe TCP connection to the specified server + #[experimental] pub fn connect(host: &str, port: u16) -> IoResult, BufferedReader>> { let socket = try!(TcpStream::connect(host, port)); - Connection::new(BufferedWriter::new(socket.clone()), BufferedReader::new(socket.clone())) + Ok(Connection::new(BufferedWriter::new(socket.clone()), BufferedReader::new(socket))) } } impl Connection where T: IrcWriter, U: IrcReader { - pub fn new(writer: T, reader: U) -> IoResult> { - Ok(Connection { - writer: RefCell::new(writer), - reader: RefCell::new(reader), - }) + /// Creates a new connection from any arbitrary IrcWriter and IrcReader + #[experimental] + pub fn new(writer: T, reader: U) -> Connection { + Connection { + writer: Mutex::new(writer), + reader: Mutex::new(reader), + } } - fn send_internal(&self, msg: &str) -> IoResult<()> { - let mut send = self.writer.borrow_mut(); - try!(send.write_str(msg)); + /// Sends a Message over this connection + #[experimental] + pub fn send(&self, message: Message) -> IoResult<()> { + let mut send = self.writer.lock(); + try!(send.write_str(message.into_string()[])); send.flush() } - pub fn send(&self, msg: Message) -> IoResult<()> { - let mut send = msg.command.to_string(); - if msg.args.init().len() > 0 { - send.push_str(" "); - send.push_str(msg.args.init().connect(" ")[]); - } - send.push_str(" "); - if msg.colon_flag.is_some() { send.push_str(":") } - send.push_str(msg.args.last().unwrap()[]); - send.push_str("\r\n"); - self.send_internal(send[]) - } - - pub fn writer<'a>(&'a self) -> RefMut<'a, T> { - self.writer.borrow_mut() - } - - pub fn reader<'a>(&'a self) -> RefMut<'a, U> { - self.reader.borrow_mut() + /// Receives a single line from this connection + #[experimental] + pub fn recv(&self) -> IoResult { + self.reader.lock().read_line() } } diff --git a/src/data.rs b/src/data.rs deleted file mode 100644 index 872f1d5..0000000 --- a/src/data.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::collections::HashMap; -use std::io::fs::File; -use std::io::{InvalidInput, IoError, IoResult}; -use serialize::json::{decode}; - -pub trait IrcWriter: Writer + Sized + 'static {} -impl IrcWriter for T where T: Writer + Sized + 'static {} -pub trait IrcReader: Buffer + Sized + 'static {} -impl IrcReader for T where T: Buffer + Sized + 'static {} - -#[deriving(PartialEq, Clone, Show)] -pub struct User { - name: String, - access_level: AccessLevel, -} - -impl User { - pub fn new(name: &str) -> User { - let rank = AccessLevel::from_str(name); - User { - name: if let Member = rank { - name.into_string() - } else { - name[1..].into_string() - }, - access_level: rank, - } - } -} - -#[deriving(PartialEq, Clone, Show)] -pub enum AccessLevel { - Owner, - Admin, - Oper, - HalfOp, - Voice, - Member, -} - -impl AccessLevel { - pub fn from_str(s: &str) -> AccessLevel { - if s.len() == 0 { Member } else { - match s.char_at(0) { - '~' => Owner, - '&' => Admin, - '@' => Oper, - '%' => HalfOp, - '+' => Voice, - _ => Member, - } - } - } -} - -#[deriving(Show, PartialEq)] -pub struct Message { - pub source: Option, - pub command: String, - pub args: Vec, - pub colon_flag: Option, -} - -impl<'a> Message { - pub fn new(source: Option<&'a str>, command: &'a str, args: Vec<&'a str>, colon_flag: Option) -> Message { - Message { - source: source.map(|s: &str| s.into_string()), - command: command.into_string(), - args: args.into_iter().map(|s: &str| s.into_string()).collect(), - colon_flag: colon_flag, - } - } -} - -#[deriving(Clone, Decodable)] -pub struct Config { - pub owners: Vec, - pub nickname: String, - pub username: String, - pub realname: String, - pub password: String, - pub server: String, - pub port: u16, - pub channels: Vec, - pub options: HashMap, -} - -impl Config { - pub fn load(path: Path) -> IoResult { - let mut file = try!(File::open(&path)); - let data = try!(file.read_to_string()); - decode(data[]).map_err(|e| IoError { - kind: InvalidInput, - desc: "Decoder error", - detail: Some(e.to_string()), - }) - } - - pub fn load_utf8(path: &str) -> IoResult { - Config::load(Path::new(path)) - } - - pub fn is_owner(&self, nickname: &str) -> bool { - self.owners[].contains(&String::from_str(nickname)) - } -} diff --git a/src/data/command.rs b/src/data/command.rs new file mode 100644 index 0000000..071a36c --- /dev/null +++ b/src/data/command.rs @@ -0,0 +1,768 @@ +//! Enumeration of all available client commands +#![stable] +use std::io::{InvalidInput, IoError, IoResult}; +use data::message::Message; + +/// List of all client commands as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812) +#[stable] +#[deriving(Show, PartialEq)] +pub enum Command<'a> { + // 3.1 Connection Registration + /// PASS password + PASS(&'a str), + /// NICK nickname + NICK(&'a str), + /// USER user mode * realname + USER(&'a str, &'a str, &'a str), + /// OPER name password + OPER(&'a str, &'a str), + /// MODE nickname modes + /// MODE channel modes [modeparams] + MODE(&'a str, &'a str, Option<&'a str>), + /// SERVICE nickname reserved distribution type reserved info + SERVICE(&'a str, &'a str, &'a str, &'a str, &'a str, &'a str), + /// QUIT Quit Message + QUIT(Option<&'a str>), + /// SQUIT server comment + SQUIT(&'a str, &'a str), + + // 3.2 Channel operations + /// JOIN chanlist [chankeys] + JOIN(&'a str, Option<&'a str>), + /// PART chanlist [Part Message] + PART(&'a str, Option<&'a str>), + // MODE is already defined. + // MODE(&'a str, &'a str, Option<&'a str>), + /// TOPIC channel [topic] + TOPIC(&'a str, Option<&'a str>), + /// NAMES [chanlist [target]] + NAMES(Option<&'a str>, Option<&'a str>), + /// LIST [chanlist [target]] + LIST(Option<&'a str>, Option<&'a str>), + /// INVITE nickname channel + INVITE(&'a str, &'a str), + /// KICK chanlist userlist [comment] + KICK(&'a str, &'a str, Option<&'a str>), + + // 3.3 Sending messages + /// PRIVMSG msgtarget text to be sent + PRIVMSG(&'a str, &'a str), + /// NOTICE msgtarget text + NOTICE(&'a str, &'a str), + + // 3.4 Server queries and commands + /// MOTD [target] + MOTD(Option<&'a str>), + /// LUSERS [mask [target]] + LUSERS(Option<&'a str>, Option<&'a str>), + /// VERSION [target] + VERSION(Option<&'a str>), + /// STATS [query [target]] + STATS(Option<&'a str>, Option<&'a str>), + /// LINKS [[remote server] server mask] + LINKS(Option<&'a str>, Option<&'a str>), + /// TIME [target] + TIME(Option<&'a str>), + /// CONNECT target server port [remote server] + CONNECT(&'a str, &'a str, Option<&'a str>), + /// TRACE [target] + TRACE(Option<&'a str>), + /// ADMIN [target] + ADMIN(Option<&'a str>), + /// INFO [target] + INFO(Option<&'a str>), + + // 3.5 Service Query and Commands + /// SERVLIST [mask [type]] + SERVLIST(Option<&'a str>, Option<&'a str>), + /// SQUERY servicename text + SQUERY(&'a str, &'a str), + + // 3.6 User based queries + /// WHO [mask ["o"]] + WHO(Option<&'a str>, Option), + /// WHOIS [target] masklist + WHOIS(Option<&'a str>, &'a str), + /// WHOWAS nicklist [count [target]] + WHOWAS(&'a str, Option<&'a str>, Option<&'a str>), + + // 3.7 Miscellaneous messages + /// KILL nickname comment + KILL(&'a str, &'a str), + /// PING server1 [server2] + PING(&'a str, Option<&'a str>), + /// PONG server [server2] + PONG(&'a str, Option<&'a str>), + /// ERROR error message + ERROR(&'a str), + + + // 4 Optional Features + /// AWAY [text] + AWAY(Option<&'a str>), + /// REHASH + REHASH, + /// DIE + DIE, + /// RESTART + RESTART, + /// SUMMON user [target [channel]] + SUMMON(&'a str, Option<&'a str>, Option<&'a str>), + /// USERS [target] + USERS(Option<&'a str>), + /// WALLOPS Text to be sent + WALLOPS(&'a str), + /// USERHOST space-separated nicklist + USERHOST(Vec<&'a str>), + /// ISON space-separated nicklist + ISON(Vec<&'a str>), + + // Non-RFC commands from InspIRCd + /// SAJOIN nickname channel + SAJOIN(&'a str, &'a str), + /// SAMODE target modes [modeparams] + SAMODE(&'a str, &'a str, Option<&'a str>), + /// SANICK old nickname new nickname + SANICK(&'a str, &'a str), + /// SAPART nickname reason + SAPART(&'a str, &'a str), + /// SAQUIT nickname reason + SAQUIT(&'a str, &'a str), +} + +impl<'a> Command<'a> { + /// Converts a Command into a Message + #[stable] + pub fn to_message(self) -> Message { + match self { + PASS(p) => Message::new(None, "PASS", None, Some(p)), + NICK(n) => Message::new(None, "NICK", None, Some(n)), + USER(u, m, r) => Message::new(None, "USER", Some(vec![u, m, "*"]), Some(r)), + OPER(u, p) => Message::new(None, "OPER", Some(vec![u]), Some(p)), + MODE(t, m, Some(p)) => Message::new(None, "MODE", Some(vec![t, m, p]), None), + MODE(t, m, None) => Message::new(None, "MODE", Some(vec![t, m]), None), + SERVICE(n, r, d, t, re, i) => Message::new(None, "SERVICE", + Some(vec![n, r, d, t, re]), Some(i)), + QUIT(Some(m)) => Message::new(None, "QUIT", None, Some(m)), + QUIT(None) => Message::new(None, "QUIT", None, None), + SQUIT(s, c) => Message::new(None, "SQUIT", Some(vec![s]), Some(c)), + JOIN(c, Some(k)) => Message::new(None, "JOIN", Some(vec![c, k]), None), + JOIN(c, None) => Message::new(None, "JOIN", Some(vec![c]), None), + PART(c, Some(m)) => Message::new(None, "PART", Some(vec![c]), Some(m)), + PART(c, None) => Message::new(None, "PART", Some(vec![c]), None), + TOPIC(c, Some(t)) => Message::new(None, "TOPIC", Some(vec![c]), Some(t)), + TOPIC(c, None) => Message::new(None, "TOPIC", Some(vec![c]), None), + NAMES(Some(c), Some(t)) => Message::new(None, "NAMES", Some(vec![c]), Some(t)), + NAMES(Some(c), None) => Message::new(None, "NAMES", Some(vec![c]), None), + NAMES(None, _) => Message::new(None, "NAMES", None, None), + LIST(Some(c), Some(t)) => Message::new(None, "LIST", Some(vec![c]), Some(t)), + LIST(Some(c), None) => Message::new(None, "LIST", Some(vec![c]), None), + LIST(None, _) => Message::new(None, "LIST", None, None), + INVITE(n, c) => Message::new(None, "INVITE", Some(vec![n, c]), None), + KICK(c, n, Some(r)) => Message::new(None, "KICK", Some(vec![c, n]), Some(r)), + KICK(c, n, None) => Message::new(None, "KICK", Some(vec![c, n]), None), + PRIVMSG(t, m) => Message::new(None, "PRIVMSG", Some(vec![t]), Some(m)), + NOTICE(t, m) => Message::new(None, "NOTICE", Some(vec![t]), Some(m)), + MOTD(Some(t)) => Message::new(None, "MOTD", None, Some(t)), + MOTD(None) => Message::new(None, "MOTD", None, None), + LUSERS(Some(m), Some(t)) => Message::new(None, "LUSERS", Some(vec![m]), Some(t)), + LUSERS(Some(m), None) => Message::new(None, "LUSERS", Some(vec![m]), None), + LUSERS(None, _) => Message::new(None, "LUSERS", None, None), + VERSION(Some(t)) => Message::new(None, "VERSION", None, Some(t)), + VERSION(None) => Message::new(None, "VERSION", None, None), + STATS(Some(q), Some(t)) => Message::new(None, "STATS", Some(vec![q]), Some(t)), + STATS(Some(q), None) => Message::new(None, "STATS", Some(vec![q]), None), + STATS(None, _) => Message::new(None, "STATS", None, None), + LINKS(Some(r), Some(s)) => Message::new(None, "LINKS", Some(vec![r]), Some(s)), + LINKS(None, Some(s)) => Message::new(None, "LINKS", None, Some(s)), + LINKS(_, None) => Message::new(None, "LINKS", None, None), + TIME(Some(t)) => Message::new(None, "TIME", None, Some(t)), + TIME(None) => Message::new(None, "TIME", None, None), + CONNECT(t, p, Some(r)) => Message::new(None, "CONNECT", Some(vec![t, p]), Some(r)), + CONNECT(t, p, None) => Message::new(None, "CONNECT", Some(vec![t, p]), None), + TRACE(Some(t)) => Message::new(None, "TRACE", None, Some(t)), + TRACE(None) => Message::new(None, "TRACE", None, None), + ADMIN(Some(t)) => Message::new(None, "ADMIN", None, Some(t)), + ADMIN(None) => Message::new(None, "ADMIN", None, None), + INFO(Some(t)) => Message::new(None, "INFO", None, Some(t)), + INFO(None) => Message::new(None, "INFO", None, None), + SERVLIST(Some(m), Some(t)) => Message::new(None, "SERVLIST", Some(vec![m]), Some(t)), + SERVLIST(Some(m), None) => Message::new(None, "SERVLIST", Some(vec![m]), None), + SERVLIST(None, _) => Message::new(None, "SERVLIST", None, None), + SQUERY(s, t) => Message::new(None, "SQUERY", Some(vec![s, t]), None), + WHO(Some(s), Some(true)) => Message::new(None, "WHO", Some(vec![s, "o"]), None), + WHO(Some(s), _) => Message::new(None, "WHO", Some(vec![s]), None), + WHO(None, _) => Message::new(None, "WHO", None, None), + WHOIS(Some(t), m) => Message::new(None, "WHOIS", Some(vec![t, m]), None), + WHOIS(None, m) => Message::new(None, "WHOIS", Some(vec![m]), None), + WHOWAS(n, Some(c), Some(t)) => Message::new(None, "WHOWAS", Some(vec![n, c]), Some(t)), + WHOWAS(n, Some(c), None) => Message::new(None, "WHOWAS", Some(vec![n, c]), None), + WHOWAS(n, None, _) => Message::new(None, "WHOWAS", Some(vec![n]), None), + KILL(n, c) => Message::new(None, "KILL", Some(vec![n]), Some(c)), + PING(s, Some(t)) => Message::new(None, "PING", Some(vec![s]), Some(t)), + PING(s, None) => Message::new(None, "PING", None, Some(s)), + PONG(s, Some(t)) => Message::new(None, "PONG", Some(vec![s]), Some(t)), + PONG(s, None) => Message::new(None, "PONG", None, Some(s)), + ERROR(m) => Message::new(None, "ERROR", None, Some(m)), + AWAY(Some(m)) => Message::new(None, "AWAY", None, Some(m)), + AWAY(None) => Message::new(None, "AWAY", None, None), + REHASH => Message::new(None, "REHASH", None, None), + DIE => Message::new(None, "DIE", None, None), + RESTART => Message::new(None, "RESTART", None, None), + SUMMON(u, Some(t), Some(c)) => Message::new(None, "SUMMON", Some(vec![u, t]), Some(c)), + SUMMON(u, Some(t), None) => Message::new(None, "SUMMON", Some(vec![u, t]), None), + SUMMON(u, None, _) => Message::new(None, "SUMMON", Some(vec![u]), None), + USERS(Some(t)) => Message::new(None, "USERS", None, Some(t)), + USERS(None) => Message::new(None, "USERS", None, None), + WALLOPS(t) => Message::new(None, "WALLOPS", None, Some(t)), + USERHOST(u) => Message::new(None, "USERHOST", Some(u), None), + ISON(u) => Message::new(None, "ISON", Some(u), None), + SAJOIN(n, c) => Message::new(None, "SAJOIN", Some(vec![n, c]), None), + SAMODE(t, m, Some(p)) => Message::new(None, "SAMODE", Some(vec![t, m, p]), None), + SAMODE(t, m, None) => Message::new(None, "SAMODE", Some(vec![t, m]), None), + SANICK(o, n) => Message::new(None, "SANICK", Some(vec![o, n]), None), + SAPART(c, r) => Message::new(None, "SAPART", Some(vec![c]), Some(r)), + SAQUIT(c, r) => Message::new(None, "SAQUIT", Some(vec![c]), Some(r)), + } + } + + /// Converts a Message into a Command + #[stable] + pub fn from_message(m: &'a Message) -> IoResult> { + /* FIXME: Re-write this using match as so: + if let "PASS" = m.command[] { + match m.suffix { + Some(ref suffix) => { + if m.args.len() != 0 { return Err(invalid_input()) } + PASS(suffix[]) + }, + None => { + if m.args.len() != 1 { return Err(invalid_input()) } + PASS(m.args[0][]) + } + } + } + */ + Ok(if let "PASS" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 0 { return Err(invalid_input()) } + PASS(m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 1 { return Err(invalid_input()) } + PASS(m.args[0][]) + } + } else if let "NICK" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 0 { return Err(invalid_input()) } + NICK(m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 1 { return Err(invalid_input()) } + NICK(m.args[0][]) + } + } else if let "USER" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 2 { return Err(invalid_input()) } + USER(m.args[0][], m.args[1][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 3 { return Err(invalid_input()) } + USER(m.args[0][], m.args[1][], m.args[2][]) + } + } else if let "OPER" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + OPER(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + OPER(m.args[0][], m.args[1][]) + } + } else if let "MODE" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 2 { return Err(invalid_input()) } + MODE(m.args[0][], m.args[1][], Some(m.suffix.as_ref().unwrap()[])) + } else if m.args.len() == 3 { + MODE(m.args[0][], m.args[1][], Some(m.args[2][])) + } else if m.args.len() == 2 { + MODE(m.args[0][], m.args[1][], None) + } else { + return Err(invalid_input()) + } + } else if let "SERVICE" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 5 { return Err(invalid_input()) } + SERVICE(m.args[0][], m.args[1][], m.args[2][], m.args[3][], m.args[4][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 6 { return Err(invalid_input()) } + SERVICE(m.args[0][], m.args[1][], m.args[2][], m.args[3][], m.args[4][], m.args[5][]) + } + } else if let "QUIT" = m.command[] { + if m.args.len() != 0 { return Err(invalid_input()) } + if m.suffix.is_some() { + QUIT(Some(m.suffix.as_ref().unwrap()[])) + } else { + QUIT(None) + } + } else if let "SQUIT" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + SQUIT(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + SQUIT(m.args[0][], m.args[1][]) + } + } else if let "JOIN" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + JOIN(m.suffix.as_ref().unwrap()[], None) + } else if m.args.len() == 1 { + JOIN(m.args[0][], Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 1 { + JOIN(m.args[0][], None) + } else if m.args.len() == 2 { + JOIN(m.args[0][], Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "PART" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + PART(m.suffix.as_ref().unwrap()[], None) + } else if m.args.len() == 1 { + PART(m.args[0][], Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 1 { + PART(m.args[0][], None) + } else if m.args.len() == 2 { + PART(m.args[0][], Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "TOPIC" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + TOPIC(m.suffix.as_ref().unwrap()[], None) + } else if m.args.len() == 1 { + TOPIC(m.args[0][], Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 1 { + TOPIC(m.args[0][], None) + } else if m.args.len() == 2 { + TOPIC(m.args[0][], Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "NAMES" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + NAMES(Some(m.suffix.as_ref().unwrap()[]), None) + } else if m.args.len() == 1 { + NAMES(Some(m.args[0][]), Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 0 { + NAMES(None, None) + } else if m.args.len() == 1 { + NAMES(Some(m.args[0][]), None) + } else if m.args.len() == 2 { + NAMES(Some(m.args[0][]), Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "LIST" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + LIST(Some(m.suffix.as_ref().unwrap()[]), None) + } else if m.args.len() == 1 { + LIST(Some(m.args[0][]), Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 0 { + LIST(None, None) + } else if m.args.len() == 1 { + LIST(Some(m.args[0][]), None) + } else if m.args.len() == 2 { + LIST(Some(m.args[0][]), Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "INVITE" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + INVITE(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + INVITE(m.args[0][], m.args[1][]) + } + } else if let "KICK" = m.command[] { + if m.args.len() != 2 { return Err(invalid_input()) } + KICK(m.args[0][], m.args[1][], m.suffix.as_ref().map(|s| s[])) + } else if let "PRIVMSG" = m.command[] { + if !m.suffix.is_some() || m.args.len() != 1 { return Err(invalid_input()) } + PRIVMSG(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else if let "NOTICE" = m.command[] { + if !m.suffix.is_some() || m.args.len() != 1 { return Err(invalid_input()) } + NOTICE(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else if let "MOTD" = m.command[] { + if m.args.len() != 0 { return Err(invalid_input()) } + if m.suffix.is_some() { + MOTD(Some(m.suffix.as_ref().unwrap()[])) + } else { + MOTD(None) + } + } else if let "LUSERS" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + LUSERS(Some(m.suffix.as_ref().unwrap()[]), None) + } else if m.args.len() == 1 { + LUSERS(Some(m.args[0][]), Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 0 { + LUSERS(None, None) + } else if m.args.len() == 1 { + LUSERS(Some(m.args[0][]), None) + } else if m.args.len() == 2 { + LUSERS(Some(m.args[0][]), Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "VERSION" = m.command[] { + if m.args.len() != 0 { return Err(invalid_input()) } + if m.suffix.is_some() { + VERSION(Some(m.suffix.as_ref().unwrap()[])) + } else { + VERSION(None) + } + } else if let "STATS" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + STATS(Some(m.suffix.as_ref().unwrap()[]), None) + } else if m.args.len() == 1 { + STATS(Some(m.args[0][]), Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 0 { + STATS(None, None) + } else if m.args.len() == 1 { + STATS(Some(m.args[0][]), None) + } else if m.args.len() == 2 { + STATS(Some(m.args[0][]), Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "LINKS" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + LINKS(None, Some(m.suffix.as_ref().unwrap()[])) + } else if m.args.len() == 1 { + LINKS(Some(m.args[0][]), Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 0 { + LINKS(None, None) + } else { + return Err(invalid_input()) + } + } else if let "TIME" = m.command[] { + if m.args.len() != 0 { return Err(invalid_input()) } + if m.suffix.is_some() { + TIME(Some(m.suffix.as_ref().unwrap()[])) + } else { + TIME(None) + } + } else if let "CONNECT" = m.command[] { + if m.args.len() != 2 { return Err(invalid_input()) } + CONNECT(m.args[0][], m.args[1][], m.suffix.as_ref().map(|s| s[])) + } else if let "TRACE" = m.command[] { + if m.args.len() != 0 { return Err(invalid_input()) } + if m.suffix.is_some() { + TRACE(Some(m.suffix.as_ref().unwrap()[])) + } else { + TRACE(None) + } + } else if let "ADMIN" = m.command[] { + if m.args.len() != 0 { return Err(invalid_input()) } + if m.suffix.is_some() { + TIME(Some(m.suffix.as_ref().unwrap()[])) + } else { + TIME(None) + } + } else if let "INFO" = m.command[] { + if m.args.len() != 0 { return Err(invalid_input()) } + if m.suffix.is_some() { + TIME(Some(m.suffix.as_ref().unwrap()[])) + } else { + TIME(None) + } + } else if let "SERVLIST" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + SERVLIST(Some(m.suffix.as_ref().unwrap()[]), None) + } else if m.args.len() == 1 { + SERVLIST(Some(m.args[0][]), Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 0 { + SERVLIST(None, None) + } else if m.args.len() == 1 { + SERVLIST(Some(m.args[0][]), None) + } else if m.args.len() == 2 { + SERVLIST(Some(m.args[0][]), Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "SQUERY" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + SQUERY(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + SQUERY(m.args[0][], m.args[1][]) + } + } else if let "WHO" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + WHO(Some(m.suffix.as_ref().unwrap()[]), None) + } else if m.args.len() == 1 { + WHO(Some(m.args[0][]), Some(m.suffix.as_ref().unwrap()[] == "o")) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 0 { + WHO(None, None) + } else if m.args.len() == 1 { + WHO(Some(m.args[0][]), None) + } else if m.args.len() == 2 { + WHO(Some(m.args[0][]), Some(m.args[1][] == "o")) + } else { + return Err(invalid_input()) + } + } else if let "WHOIS" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + WHOIS(None, m.suffix.as_ref().unwrap()[]) + } else if m.args.len() == 1 { + WHOIS(Some(m.args[0][]), m.suffix.as_ref().unwrap()[]) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 1 { + WHOIS(None, m.args[0][]) + } else if m.args.len() == 2 { + WHOIS(Some(m.args[0][]), m.args[1][]) + } else { + return Err(invalid_input()) + } + } else if let "WHOWAS" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + WHOWAS(m.suffix.as_ref().unwrap()[], None, None) + } else if m.args.len() == 1 { + WHOWAS(m.args[0][], None, Some(m.suffix.as_ref().unwrap()[])) + } else if m.args.len() == 2 { + WHOWAS(m.args[0][], Some(m.args[1][]), Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 1 { + WHOWAS(m.args[0][], None, None) + } else if m.args.len() == 2 { + WHOWAS(m.args[0][], None, Some(m.args[1][])) + } else if m.args.len() == 3 { + WHOWAS(m.args[0][], Some(m.args[1][]), Some(m.args[2][])) + } else { + return Err(invalid_input()) + } + } else if let "KILL" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + KILL(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + KILL(m.args[0][], m.args[1][]) + } + } else if let "PING" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + PING(m.suffix.as_ref().unwrap()[], None) + } else if m.args.len() == 1 { + PING(m.args[0][], Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 1 { + PING(m.args[0][], None) + } else if m.args.len() == 2 { + PING(m.args[0][], Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "PONG" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + PONG(m.suffix.as_ref().unwrap()[], None) + } else if m.args.len() == 1 { + PONG(m.args[0][], Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 1 { + PONG(m.args[0][], None) + } else if m.args.len() == 2 { + PONG(m.args[0][], Some(m.args[1][])) + } else { + return Err(invalid_input()) + } + } else if let "ERROR" = m.command[] { + if m.suffix.is_some() && m.args.len() == 0 { + ERROR(m.suffix.as_ref().unwrap()[]) + } else { + return Err(invalid_input()) + } + } else if let "AWAY" = m.command[] { + if m.args.len() == 0 { + AWAY(m.suffix.as_ref().map(|s| s[])) + } else { + return Err(invalid_input()) + } + } else if let "REHASH" = m.command[] { + if m.args.len() == 0 { + REHASH + } else { + return Err(invalid_input()) + } + } else if let "DIE" = m.command[] { + if m.args.len() == 0 { + DIE + } else { + return Err(invalid_input()) + } + } else if let "RESTART" = m.command[] { + if m.args.len() == 0 { + RESTART + } else { + return Err(invalid_input()) + } + } else if let "SUMMON" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 0 { + SUMMON(m.suffix.as_ref().unwrap()[], None, None) + } else if m.args.len() == 1 { + SUMMON(m.args[0][], Some(m.suffix.as_ref().unwrap()[]), None) + } else if m.args.len() == 2 { + SUMMON(m.args[0][], Some(m.args[1][]), Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 1 { + SUMMON(m.args[0][], None, None) + } else if m.args.len() == 2 { + SUMMON(m.args[0][], Some(m.args[1][]), None) + } else if m.args.len() == 3 { + SUMMON(m.args[0][], Some(m.args[1][]), Some(m.args[2][])) + } else { + return Err(invalid_input()) + } + } else if let "USERS" = m.command[] { + if m.args.len() == 0 { + USERS(m.suffix.as_ref().map(|s| s[])) + } else if m.args.len() == 1 { + USERS(Some(m.args[0][])) + } else { + return Err(invalid_input()) + } + } else if let "WALLOPS" = m.command[] { + if m.suffix.is_some() && m.args.len() == 0 { + WALLOPS(m.suffix.as_ref().unwrap()[]) + } else if m.args.len() == 1 { + WALLOPS(m.args[0][]) + } else { + return Err(invalid_input()) + } + } else if let "USERHOST" = m.command[] { + if m.suffix.is_none() { + USERHOST(m.args.iter().map(|s| s[]).collect()) + } else { + return Err(invalid_input()) + } + } else if let "ISON" = m.command[] { + if m.suffix.is_none() { + USERHOST(m.args.iter().map(|s| s[]).collect()) + } else { + return Err(invalid_input()) + } + } else if let "SAJOIN" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + SAJOIN(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + SAJOIN(m.args[0][], m.args[1][]) + } + } else if let "SAMODE" = m.command[] { + if m.suffix.is_some() { + if m.args.len() == 1 { + SAMODE(m.args[0][], m.suffix.as_ref().unwrap()[], None) + } else if m.args.len() == 2 { + SAMODE(m.args[0][], m.args[1][], Some(m.suffix.as_ref().unwrap()[])) + } else { + return Err(invalid_input()) + } + } else if m.args.len() == 2 { + SAMODE(m.args[0][], m.args[1][], None) + } else if m.args.len() == 3 { + SAMODE(m.args[0][], m.args[1][], Some(m.args[2][])) + } else { + return Err(invalid_input()) + } + } else if let "SANICK" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + SANICK(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + SANICK(m.args[0][], m.args[1][]) + } + } else if let "SAPART" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + SAPART(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + SAPART(m.args[0][], m.args[1][]) + } + } else if let "SAQUIT" = m.command[] { + if m.suffix.is_some() { + if m.args.len() != 1 { return Err(invalid_input()) } + SAQUIT(m.args[0][], m.suffix.as_ref().unwrap()[]) + } else { + if m.args.len() != 2 { return Err(invalid_input()) } + SAQUIT(m.args[0][], m.args[1][]) + } + } else { + return Err(invalid_input()) + }) + } +} + +/// Produces an invalid_input IoError +#[stable] +fn invalid_input() -> IoError { + IoError { + kind: InvalidInput, + desc: "Failed to parse malformed message as command.", + detail: None + } +} diff --git a/src/data/config.rs b/src/data/config.rs new file mode 100644 index 0000000..3b65f26 --- /dev/null +++ b/src/data/config.rs @@ -0,0 +1,56 @@ +//! JSON configuration files using libserialize +#![stable] +use std::collections::HashMap; +use std::io::fs::File; +use std::io::{InvalidInput, IoError, IoResult}; +use serialize::json::decode; + +/// Configuration data +#[deriving(Clone, Decodable)] +#[unstable] +pub struct Config { + /// A list of the owners of the bot by nickname + pub owners: Vec, + /// The bot's nickname + pub nickname: String, + /// The bot's username + pub username: String, + /// The bot's real name + pub realname: String, + /// The bot's password + pub password: String, + /// The server to connect to + pub server: String, + /// The port to connect on + pub port: u16, + /// A list of channels to join on connection + pub channels: Vec, + /// A map of additional options to be stored in config + pub options: HashMap, +} + +impl Config { + /// Loads a JSON configuration from the desired path. + #[stable] + pub fn load(path: Path) -> IoResult { + let mut file = try!(File::open(&path)); + let data = try!(file.read_to_string()); + decode(data[]).map_err(|e| IoError { + kind: InvalidInput, + desc: "Failed to decode configuration file.", + detail: Some(e.to_string()), + }) + } + + /// Loads a JSON configuration using the string as a UTF-8 path. + #[stable] + pub fn load_utf8(path: &str) -> IoResult { + Config::load(Path::new(path)) + } + + /// Determines whether or not the nickname provided is the owner of the bot. + #[stable] + pub fn is_owner(&self, nickname: &str) -> bool { + self.owners[].contains(&String::from_str(nickname)) + } +} diff --git a/src/data/message.rs b/src/data/message.rs new file mode 100644 index 0000000..7039608 --- /dev/null +++ b/src/data/message.rs @@ -0,0 +1,84 @@ +//! Messages to and from the server +#![experimental] +use std::from_str::FromStr; + +/// IRC Message data +#[experimental] +#[deriving(Clone, PartialEq, Show)] +pub struct Message { + /// The message prefix (or source) as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812) + pub prefix: Option, + /// The IRC command as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812) + pub command: String, + /// The command arguments + pub args: Vec, + /// The message suffix as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812) + /// This is the only part of the message that is allowed to contain spaces. + pub suffix: Option, +} + +impl Message { + /// Creates a new Message + #[experimental] + pub fn new(prefix: Option<&str>, command: &str, args: Option>, suffix: Option<&str>) + -> Message { + Message { + prefix: prefix.map(|s| s.into_string()), + command: command.into_string(), + args: args.map_or(Vec::new(), |v| v.iter().map(|s| s.into_string()).collect()), + suffix: suffix.map(|s| s.into_string()), + } + } + + /// Converts a Message into a String according to the IRC protocol + #[experimental] + pub fn into_string(&self) -> String { + let mut ret = String::new(); + if let Some(ref prefix) = self.prefix { + ret.push(':'); + ret.push_str(prefix[]); + ret.push(' '); + } + ret.push_str(self.command[]); + for arg in self.args.iter() { + ret.push(' '); + ret.push_str(arg[]); + } + if let Some(ref suffix) = self.suffix { + ret.push_str(" :"); + ret.push_str(suffix[]); + } + ret.push_str("\r\n"); + ret + } +} + +impl FromStr for Message { + fn from_str(s: &str) -> Option { + let mut state = s.clone(); + if s.len() == 0 { return 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 suffix = if state.contains(":") { + let suffix = state.find(':').map(|i| state[i+1..state.len()-2]); + state = state.find(':').map_or("", |i| state[..i]); + suffix + } else { + None + }; + let command = match state.find(' ').map(|i| state[..i]) { + Some(cmd) => { + state = state.find(' ').map_or("", |i| state[i+1..]); + cmd + } + _ => return None + }; + let args: Vec<_> = state.splitn(14, ' ').filter(|s| s.len() != 0).collect(); + Some(Message::new(prefix, command, if args.len() > 0 { Some(args) } else { None }, suffix)) + } +} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..474ba64 --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,20 @@ +//! Data related to IRC functionality +#![experimental] + +pub mod kinds { + //! Trait definitions of appropriate Writers and Buffers for use with this library + #![unstable] + + /// Trait describing all possible Writers for this library + #[unstable] + pub trait IrcWriter: Writer + Sized + Send + 'static {} + impl IrcWriter for T where T: Writer + Sized + Send + 'static {} + /// Trait describing all possible Readers for this library + #[unstable] + pub trait IrcReader: Buffer + Sized + Send + 'static {} + impl IrcReader for T where T: Buffer + Sized + Send + 'static {} +} + +pub mod command; +pub mod config; +pub mod message; diff --git a/src/lib.rs b/src/lib.rs index e2a53ce..8c902f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,70 +1,12 @@ +//! A simple, thread-safe IRC client library. +#![crate_name = "irc"] +#![crate_type = "lib"] +#![unstable] + #![feature(if_let)] -#![feature(phase)] #![feature(slicing_syntax)] -extern crate regex; -#[phase(plugin)] extern crate regex_macros; extern crate serialize; -use std::io::{InvalidInput, IoError, IoResult}; - -pub mod bot; -pub mod conn; + mod conn; pub mod data; - -pub trait Server<'a>: Iterator { - fn send(&self, message: data::Message) -> IoResult<()>; - fn config(&self) -> &data::Config; - fn get_users(&self, chan: &str) -> Option>; -} - -fn process(msg: &str) -> IoResult<(&str, &str, Vec<&str>)> { - let reg = regex!(r"^(?::([^ ]+) )?([^ ]+)(.*)"); - let cap = match reg.captures(msg) { - Some(x) => x, - None => return Err(IoError { - kind: InvalidInput, - desc: "Failed to parse line", - detail: None, - }), - }; - let source = cap.at(1); - let command = cap.at(2); - let args = parse_args(cap.at(3)); - Ok((source, command, args)) -} - -fn parse_args(line: &str) -> Vec<&str> { - let reg = regex!(r" ([^: ]+)| :([^\r\n]*)[\r\n]*$"); - reg.captures_iter(line).map(|cap| { - match cap.at(1) { - "" => cap.at(2), - x => x, - } - }).collect() -} - -#[cfg(test)] -mod test { - use super::{process, parse_args}; - - #[test] - fn process_line() { - let res = process(":flare.to.ca.fyrechat.net 353 pickles = #pickles :pickles awe\r\n").unwrap(); - let (source, command, args) = res; - assert_eq!(source, "flare.to.ca.fyrechat.net"); - assert_eq!(command, "353"); - assert_eq!(args, vec!["pickles", "=", "#pickles", "pickles awe"]); - - let res = process("PING :flare.to.ca.fyrechat.net\r\n").unwrap(); - let (source, command, args) = res; - assert_eq!(source, ""); - assert_eq!(command, "PING"); - assert_eq!(args, vec!["flare.to.ca.fyrechat.net"]); - } - - #[test] - fn process_args() { - let res = parse_args("PRIVMSG #vana :hi"); - assert_eq!(res, vec!["#vana", "hi"]) - } -} +pub mod server; diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..249e09c --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,113 @@ +//! Interface for working with IRC Servers +#![experimental] +use std::io::{BufferedReader, BufferedWriter, IoResult, TcpStream}; +use conn::Connection; +use data::command::Command; +use data::config::Config; +use data::kinds::{IrcReader, IrcWriter}; +use data::message::Message; + +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>; +} + +/// A thread-safe implementation of an IRC Server connection +#[experimental] +pub struct IrcServer<'a, T, U> where T: IrcWriter, U: IrcReader { + /// The thread-safe IRC connection + conn: Connection, + /// The configuration used with this connection + config: Config +} + +impl<'a> IrcServer<'a, BufferedWriter, BufferedReader> { + /// Creates a new IRC Server connection from the configuration at the specified path, connecting immediately. + #[experimental] + pub fn new(config: &str) -> IoResult, BufferedReader>> { + let config = try!(Config::load_utf8(config)); + let conn = try!(Connection::connect(config.server[], config.port)); + IrcServer::from_connection(config, conn) + } + + /// Creates a new IRC server connection from the specified configuration, connecting immediately. + #[experimental] + pub fn from_config(config: Config) -> IoResult, BufferedReader>> { + let conn = try!(Connection::connect(config.server[], config.port)); + IrcServer::from_connection(config, conn) + } +} + +impl<'a, T, U> Server<'a, T, U> for IrcServer<'a, T, U> where T: IrcWriter, U: IrcReader { + fn config(&self) -> &Config { + &self.config + } + + fn send(&self, command: Command) -> IoResult<()> { + self.conn.send(command.to_message()) + } + + fn iter(&'a self) -> ServerIterator<'a, T, U> { + ServerIterator::new(self) + } +} + +impl<'a, T, U> IrcServer<'a, T, U> where T: IrcWriter, U: IrcReader { + /// Creates an IRC server from the specified configuration, and any arbitrary Connection + #[experimental] + pub fn from_connection(config: Config, conn: Connection) -> IoResult> { + Ok(IrcServer { + conn: conn, + config: config + }) + } + + fn handle_message(&self, message: &Message) { + if message.command[] == "PING" { + utils::send_pong(self, message.suffix.as_ref().unwrap()[]).unwrap(); + } else if message.command[] == "376" || message.command[] == "422" { + for chan in self.config.channels.iter() { + utils::send_join(self, chan[]).unwrap(); + } + } + /* TODO: implement more message handling */ + } +} + +/// An Iterator over an IrcServer's incoming Messages +#[experimental] +pub struct ServerIterator<'a, T, U> where T: IrcWriter, U: IrcReader { + pub server: &'a IrcServer<'a, T, U> +} + +impl<'a, T, U> ServerIterator<'a, T, U> where T: IrcWriter, U: IrcReader { + /// Creates a new ServerIterator for the desired IrcServer + #[experimental] + pub fn new(server: &'a IrcServer<'a, T, U>) -> ServerIterator<'a, T, U> { + ServerIterator { + server: server + } + } +} + +impl<'a, T, U> Iterator for ServerIterator<'a, T, U> where T: IrcWriter, U: IrcReader { + fn next(&mut self) -> Option { + let line = self.server.conn.recv(); + match line { + Err(_) => None, + Ok(msg) => { + let message = from_str(msg[]); + self.server.handle_message(message.as_ref().unwrap()); + message + } + } + } +} diff --git a/src/server/utils.rs b/src/server/utils.rs new file mode 100644 index 0000000..2e44829 --- /dev/null +++ b/src/server/utils.rs @@ -0,0 +1,21 @@ +//! Utilities and shortcuts for working with IRC servers +#![experimental] + +use std::io::IoResult; +use data::command::{JOIN, NICK, PONG, USER}; +use data::kinds::{IrcReader, IrcWriter}; +use server::Server; + +/// Sends a NICK and USER to identify +pub fn identify<'a, T, U>(server: &Server<'a, T, U>) -> IoResult<()> where T: IrcWriter, U: IrcReader { + try!(server.send(NICK(server.config().nickname[]))); + server.send(USER(server.config().username[], "0", server.config().realname[])) +} + +pub fn send_pong<'a, T, U>(server: &Server<'a, T, U>, msg: &str) -> IoResult<()> where T: IrcWriter, U: IrcReader { + server.send(PONG(msg, None)) +} + +pub fn send_join<'a, T, U>(server: &Server<'a, T, U>, chanlist: &str) -> IoResult<()> where T: IrcWriter, U: IrcReader { + server.send(JOIN(chanlist, None)) +}