Performed the great Command redesign (fixes #16).

This commit is contained in:
Aaron Weiss 2016-01-30 04:56:32 -05:00
parent 81a807f0a1
commit 2eb0e63d59
5 changed files with 605 additions and 665 deletions

View file

@ -257,7 +257,7 @@ impl Write for NetStream {
mod test {
use super::Connection;
use std::io::{Cursor, sink};
use client::data::message::Message;
use client::data::Command::PRIVMSG;
use client::test::buf_empty;
#[cfg(feature = "encode")] use encoding::{DecoderTrap, Encoding};
#[cfg(feature = "encode")] use encoding::all::{ISO_8859_15, UTF_8};
@ -266,9 +266,7 @@ mod test {
#[cfg(not(feature = "encode"))]
fn send() {
let conn = Connection::new(buf_empty(), Vec::new());
assert!(conn.send(
Message::new(None, "PRIVMSG", Some(vec!["test"]), Some("Testing!"))
).is_ok());
assert!(conn.send(PRIVMSG("test".to_owned(), "Testing!".to_owned())).is_ok());
let data = String::from_utf8(conn.writer().to_vec()).unwrap();
assert_eq!(&data[..], "PRIVMSG test :Testing!\r\n");
}
@ -287,9 +285,7 @@ mod test {
#[cfg(feature = "encode")]
fn send_utf8() {
let conn = Connection::new(buf_empty(), Vec::new());
assert!(conn.send(
Message::new(None, "PRIVMSG", Some(vec!["test"]), Some("€ŠšŽžŒœŸ")), "UTF-8"
).is_ok());
assert!(conn.send(PRIVMSG("test".to_owned(), "€ŠšŽžŒœŸ".to_owned()), "UTF-8").is_ok());
let data = UTF_8.decode(&conn.writer(), DecoderTrap::Strict).unwrap();
assert_eq!(&data[..], "PRIVMSG test :€ŠšŽžŒœŸ\r\n");
}
@ -308,9 +304,7 @@ mod test {
#[cfg(feature = "encode")]
fn send_iso885915() {
let conn = Connection::new(buf_empty(), Vec::new());
assert!(conn.send(
Message::new(None, "PRIVMSG", Some(vec!["test"]), Some("€ŠšŽžŒœŸ")), "l9"
).is_ok());
assert!(conn.send(PRIVMSG("test".to_owned(), "€ŠšŽžŒœŸ".to_owned()), "l9").is_ok());
let data = ISO_8859_15.decode(&conn.writer(), DecoderTrap::Strict).unwrap();
assert_eq!(&data[..], "PRIVMSG test :€ŠšŽžŒœŸ\r\n");
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,8 @@
//! Messages to and from the server.
use std::borrow::ToOwned;
use std::io::{Result as IoResult};
use std::str::FromStr;
use client::data::Command;
/// IRC Message data.
#[derive(Clone, PartialEq, Debug)]
@ -9,40 +11,25 @@ pub struct Message {
pub tags: Option<Vec<Tag>>,
/// The message prefix (or source) as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812).
pub prefix: Option<String>,
/// The IRC command as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812).
pub command: String,
/// The command arguments.
pub args: Vec<String>,
/// 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<String>,
/// The IRC command.
pub command: Command,
}
impl Message {
/// Creates a new Message.
pub fn new(prefix: Option<&str>, command: &str, args: Option<Vec<&str>>, suffix: Option<&str>)
-> Message {
pub fn new(prefix: Option<&str>, command: &str, args: Vec<&str>, suffix: Option<&str>)
-> IoResult<Message> {
Message::with_tags(None, prefix, command, args, suffix)
}
/// Creates a new Message optionally including IRCv3.2 message tags.
pub fn with_tags(tags: Option<Vec<Tag>>, prefix: Option<&str>, command: &str,
args: Option<Vec<&str>>, suffix: Option<&str>) -> Message {
Message {
args: Vec<&str>, suffix: Option<&str>) -> IoResult<Message> {
Ok(Message {
tags: tags,
prefix: prefix.map(|s| s.to_owned()),
command: command.to_owned(),
args: args.map_or(Vec::new(), |v| v.iter().map(|&s| s.to_owned()).collect()),
suffix: suffix.map(|s| s.to_owned()),
}
}
/// Creates a new Message from already owned data.
pub fn from_owned(prefix: Option<String>, command: String, args: Option<Vec<String>>,
suffix: Option<String>) -> Message {
Message {
tags: None, prefix: prefix, command: command, args: args.unwrap_or(Vec::new()), suffix: suffix
}
command: try!(Command::new(command, args, suffix)),
})
}
/// Gets the nickname of the message source, if it exists.
@ -59,26 +46,26 @@ impl Message {
/// Converts a Message into a String according to the IRC protocol.
pub fn into_string(&self) -> String {
// TODO: tags
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);
}
let cmd: String = From::from(&self.command);
ret.push_str(&cmd);
ret.push_str("\r\n");
ret
}
}
impl From<Command> for Message {
fn from(cmd: Command) -> Message {
Message { tags: None, prefix: None, command: cmd }
}
}
impl FromStr for Message {
type Err = &'static str;
fn from_str(s: &str) -> Result<Message, &'static str> {
@ -118,9 +105,9 @@ impl FromStr for Message {
};
if suffix.is_none() { state = &state[..state.len() - 2] }
let args: Vec<_> = state.splitn(14, ' ').filter(|s| s.len() != 0).collect();
Ok(Message::with_tags(
tags, prefix, command, if args.len() > 0 { Some(args) } else { None }, suffix
))
Message::with_tags(
tags, prefix, command, args, suffix
).map_err(|_| "Invalid input for Command.")
}
}
@ -137,34 +124,33 @@ pub struct Tag(String, Option<String>);
#[cfg(test)]
mod test {
use super::{Message, Tag};
use client::data::Command::{PRIVMSG, Raw};
#[test]
fn new() {
let message = Message {
tags: None,
prefix: None,
command: format!("PRIVMSG"),
args: vec![format!("test")],
suffix: Some(format!("Testing!")),
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!(Message::new(None, "PRIVMSG", Some(vec!["test"]), Some("Testing!")), message);
assert_eq!(Message::new(None, "PRIVMSG", vec!["test"], Some("Testing!")).unwrap(), message)
}
#[test]
fn get_source_nickname() {
assert_eq!(Message::new(None, "PING", None, None).get_source_nickname(), None);
assert_eq!(Message::new(None, "PING", vec![], None).unwrap().get_source_nickname(), None);
assert_eq!(Message::new(
Some("irc.test.net"), "PING", None, None
).get_source_nickname(), None);
Some("irc.test.net"), "PING", vec![], None
).unwrap().get_source_nickname(), None);
assert_eq!(Message::new(
Some("test!test@test"), "PING", None, None
).get_source_nickname(), Some("test"));
Some("test!test@test"), "PING", vec![], None
).unwrap().get_source_nickname(), Some("test"));
assert_eq!(Message::new(
Some("test@test"), "PING", None, None
).get_source_nickname(), Some("test"));
Some("test@test"), "PING", vec![], None
).unwrap().get_source_nickname(), Some("test"));
assert_eq!(Message::new(
Some("test"), "PING", None, None
).get_source_nickname(), Some("test"));
Some("test"), "PING", vec![], None
).unwrap().get_source_nickname(), Some("test"));
}
#[test]
@ -172,17 +158,13 @@ mod test {
let message = Message {
tags: None,
prefix: None,
command: format!("PRIVMSG"),
args: vec![format!("test")],
suffix: Some(format!("Testing!")),
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!(&message.into_string()[..], "PRIVMSG test :Testing!\r\n");
let message = Message {
tags: None,
prefix: Some(format!("test!test@test")),
command: format!("PRIVMSG"),
args: vec![format!("test")],
suffix: Some(format!("Still testing!")),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
assert_eq!(&message.into_string()[..], ":test!test@test PRIVMSG test :Still testing!\r\n");
}
@ -192,17 +174,13 @@ mod test {
let message = Message {
tags: None,
prefix: None,
command: format!("PRIVMSG"),
args: vec![format!("test")],
suffix: Some(format!("Testing!")),
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!("PRIVMSG test :Testing!\r\n".parse(), Ok(message));
let message = Message {
tags: None,
prefix: Some(format!("test!test@test")),
command: format!("PRIVMSG"),
args: vec![format!("test")],
suffix: Some(format!("Still testing!")),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
assert_eq!(":test!test@test PRIVMSG test :Still testing!\r\n".parse(), Ok(message));
let message = Message {
@ -210,9 +188,7 @@ mod test {
Tag(format!("ccc"), None),
Tag(format!("example.com/ddd"), Some(format!("eee")))]),
prefix: Some(format!("test!test@test")),
command: format!("PRIVMSG"),
args: vec![format!("test")],
suffix: Some(format!("Testing with tags!")),
command: PRIVMSG(format!("test"), format!("Testing with tags!")),
};
assert_eq!("@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
tags!\r\n".parse(), Ok(message))
@ -223,18 +199,14 @@ mod test {
let message = Message {
tags: None,
prefix: None,
command: format!("PRIVMSG"),
args: vec![format!("test")],
suffix: Some(format!("Testing!")),
command: PRIVMSG(format!("test"), format!("Testing!")),
};
let msg: Message = "PRIVMSG test :Testing!\r\n".into();
assert_eq!(msg, message);
let message = Message {
tags: None,
prefix: Some(format!("test!test@test")),
command: format!("PRIVMSG"),
args: vec![format!("test")],
suffix: Some(format!("Still testing!")),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
let msg: Message = ":test!test@test PRIVMSG test :Still testing!\r\n".into();
assert_eq!(msg, message);
@ -247,9 +219,9 @@ mod test {
let message = Message {
tags: None,
prefix: Some(format!("test!test@test")),
command: format!("COMMAND"),
args: vec![format!("ARG:test")],
suffix: Some(format!("Testing!")),
command: Raw(
format!("COMMAND"), vec![format!("ARG:test")], Some(format!("Testing!"))
),
};
let msg: Message = ":test!test@test COMMAND ARG:test :Testing!\r\n".into();
assert_eq!(msg, message);

View file

@ -1,7 +1,6 @@
//! Enumeration of all the possible server responses.
#![allow(non_camel_case_types)]
use std::str::FromStr;
use client::data::message::Message;
macro_rules! make_response {
($(#[$attr:meta] $variant:ident = $value:expr),+) => {
@ -356,11 +355,6 @@ make_response! {
}
impl Response {
/// Gets a response from a message.
pub fn from_message(m: &Message) -> Option<Response> {
m.command.parse().ok()
}
/// Determines whether or not this response is an error response.
pub fn is_error(&self) -> bool {
*self as u16 >= 400
@ -385,16 +379,6 @@ impl FromStr for Response {
mod test {
use super::Response;
#[test]
fn from_message() {
assert_eq!(Response::from_message(
&":irc.test.net 353 test = #test :test\r\n".into()
).unwrap(), Response::RPL_NAMREPLY);
assert_eq!(Response::from_message(
&":irc.test.net 433 <nick> :Nickname is already in use\r\n".into()
).unwrap(), Response::ERR_NICKNAMEINUSE);
}
#[test]
fn is_error() {
assert!(!Response::RPL_NAMREPLY.is_error());

View file

@ -6,7 +6,6 @@ use std::cell::Cell;
use std::collections::HashMap;
use std::error::Error as StdError;
use std::io::{BufReader, BufWriter, Error, ErrorKind, Result};
use std::iter::Map;
use std::path::Path;
use std::sync::{Arc, Mutex, RwLock};
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
@ -28,8 +27,6 @@ pub trait Server<'a, T: IrcRead, U: IrcWrite> {
fn send<M: Into<Message>>(&self, message: M) -> Result<()> where Self: Sized;
/// Gets an Iterator over Messages received by this Server.
fn iter(&'a self) -> ServerIterator<'a, T, U>;
/// Gets an Iterator over Commands received by this Server.
fn iter_cmd(&'a self) -> ServerCmdIterator<'a, T, U>;
/// Gets a list of Users in the specified channel. This will be none if the channel is not
/// being tracked, or if tracking is not supported altogether. For best results, be sure to
/// request `multi-prefix` support from the server.
@ -171,10 +168,6 @@ impl<'a, T: IrcRead, U: IrcWrite> Server<'a, T, U> for ServerState<T, U> where C
panic!("unimplemented")
}
fn iter_cmd(&'a self) -> ServerCmdIterator<'a, T, U> {
self.iter().map(Command::from_message_io)
}
#[cfg(not(feature = "nochanlists"))]
fn list_users(&self, chan: &str) -> Option<Vec<User>> {
self.chanlists.lock().unwrap().get(&chan.to_owned()).cloned()
@ -200,10 +193,6 @@ impl<'a, T: IrcRead, U: IrcWrite> Server<'a, T, U> for IrcServer<T, U> where Con
ServerIterator::new(self)
}
fn iter_cmd(&'a self) -> ServerCmdIterator<'a, T, U> {
self.iter().map(Command::from_message_io)
}
#[cfg(not(feature = "nochanlists"))]
fn list_users(&self, chan: &str) -> Option<Vec<User>> {
self.state.chanlists.lock().unwrap().get(&chan.to_owned()).cloned()
@ -296,12 +285,51 @@ impl<T: IrcRead, U: IrcWrite> IrcServer<T, U> where Connection<T, U>: Reconnect
/// Handles messages internally for basic client functionality.
fn handle_message(&self, msg: &Message) -> Result<()> {
if let Some(resp) = Response::from_message(msg) {
match resp {
Response::RPL_NAMREPLY => if cfg!(not(feature = "nochanlists")) {
if let Some(users) = msg.suffix.clone() {
if msg.args.len() == 3 {
let ref chan = msg.args[2];
match msg.command {
PING(ref data, _) => try!(self.send_pong(&data)),
JOIN(ref chan, _, _) => if cfg!(not(feature = "nochanlists")) {
if let Some(vec) = self.chanlists().lock().unwrap().get_mut(&chan.to_owned()) {
if let Some(src) = msg.get_source_nickname() {
vec.push(User::new(src))
}
}
},
PART(ref chan, _) => if cfg!(not(feature = "nochanlists")) {
if let Some(vec) = self.chanlists().lock().unwrap().get_mut(&chan.to_owned()) {
if let Some(src) = msg.get_source_nickname() {
if let Some(n) = vec.iter().position(|x| x.get_nickname() == src) {
vec.swap_remove(n);
}
}
}
},
MODE(ref chan, ref mode, Some(ref user)) => if cfg!(not(feature = "nochanlists")) {
if let Some(vec) = self.chanlists().lock().unwrap().get_mut(chan) {
if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) {
vec[n].update_access_level(&mode)
}
}
},
PRIVMSG(ref target, ref body) => if body.starts_with("\u{001}") {
let tokens: Vec<_> = {
let end = if body.ends_with("\u{001}") {
body.len() - 1
} else {
body.len()
};
body[1..end].split(" ").collect()
};
if target.starts_with("#") {
try!(self.handle_ctcp(&target, tokens))
} else if let Some(user) = msg.get_source_nickname() {
try!(self.handle_ctcp(user, tokens))
}
},
Command::Response(Response::RPL_NAMREPLY, ref args, ref suffix) => {
if cfg!(not(feature = "nochanlists")) {
if let Some(users) = suffix.clone() {
if args.len() == 3 {
let ref chan = args[2];
for user in users.split(" ") {
let mut chanlists = self.state.chanlists.lock().unwrap();
chanlists.entry(chan.clone()).or_insert(Vec::new())
@ -309,80 +337,36 @@ impl<T: IrcRead, U: IrcWrite> IrcServer<T, U> where Connection<T, U>: Reconnect
}
}
}
},
Response::RPL_ENDOFMOTD | Response::ERR_NOMOTD => { // On connection behavior.
if self.config().nick_password() != "" {
try!(self.send(NICKSERV(
format!("IDENTIFY {}", self.config().nick_password())
)))
}
if self.config().umodes() != "" {
try!(self.send_mode(self.config().nickname(), self.config().umodes(), ""))
}
for chan in self.config().channels().into_iter() {
try!(self.send_join(chan))
}
},
Response::ERR_NICKNAMEINUSE | Response::ERR_ERRONEOUSNICKNAME => {
let alt_nicks = self.config().get_alternate_nicknames();
let mut index = self.state.alt_nick_index.write().unwrap();
if *index >= alt_nicks.len() {
panic!("All specified nicknames were in use.")
} else {
try!(self.send(NICK(alt_nicks[*index].to_owned())));
*index += 1;
}
},
_ => ()
}
Ok(())
} else if let Ok(cmd) = msg.into() {
match cmd {
PING(data, _) => try!(self.send_pong(&data)),
JOIN(chan, _, _) => if cfg!(not(feature = "nochanlists")) {
if let Some(vec) = self.chanlists().lock().unwrap().get_mut(&chan.to_owned()) {
if let Some(src) = msg.get_source_nickname() {
vec.push(User::new(src))
}
}
},
PART(chan, _) => if cfg!(not(feature = "nochanlists")) {
if let Some(vec) = self.chanlists().lock().unwrap().get_mut(&chan.to_owned()) {
if let Some(src) = msg.get_source_nickname() {
if let Some(n) = vec.iter().position(|x| x.get_nickname() == src) {
vec.swap_remove(n);
}
}
}
},
MODE(chan, mode, Some(user)) => if cfg!(not(feature = "nochanlists")) {
if let Some(vec) = self.chanlists().lock().unwrap().get_mut(&chan) {
if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) {
vec[n].update_access_level(&mode)
}
}
},
PRIVMSG(target, body) => if body.starts_with("\u{001}") {
let tokens: Vec<_> = {
let end = if body.ends_with("\u{001}") {
body.len() - 1
} else {
body.len()
};
body[1..end].split(" ").collect()
};
if target.starts_with("#") {
try!(self.handle_ctcp(&target, tokens))
} else if let Some(user) = msg.get_source_nickname() {
try!(self.handle_ctcp(user, tokens))
}
},
_ => ()
}
Ok(())
} else {
Ok(())
}
},
Command::Response(Response::RPL_ENDOFMOTD, _, _) |
Command::Response(Response::ERR_NOMOTD, _, _) => {
if self.config().nick_password() != "" {
try!(self.send(NICKSERV(
format!("IDENTIFY {}", self.config().nick_password())
)))
}
if self.config().umodes() != "" {
try!(self.send_mode(self.config().nickname(), self.config().umodes(), ""))
}
for chan in self.config().channels().into_iter() {
try!(self.send_join(chan))
}
},
Command::Response(Response::ERR_NICKNAMEINUSE, _, _) |
Command::Response(Response::ERR_ERRONEOUSNICKNAME, _, _) => {
let alt_nicks = self.config().get_alternate_nicknames();
let mut index = self.state.alt_nick_index.write().unwrap();
if *index >= alt_nicks.len() {
panic!("All specified nicknames were in use.")
} else {
try!(self.send(NICK(alt_nicks[*index].to_owned())));
*index += 1;
}
},
_ => ()
}
Ok(())
}
/// Handles CTCP requests if the CTCP feature is enabled.
@ -445,10 +429,6 @@ pub struct ServerIterator<'a, T: IrcRead + 'a, U: IrcWrite + 'a> {
server: &'a IrcServer<T, U>
}
/// An Iterator over an IrcServer's incoming Commands.
pub type ServerCmdIterator<'a, T, U> =
Map<ServerIterator<'a, T, U>, fn(Result<Message>) -> Result<Command>>;
impl<'a, T: IrcRead + 'a, U: IrcWrite + 'a> ServerIterator<'a, T, U> where Connection<T, U>: Reconnect {
/// Creates a new ServerIterator for the desired IrcServer.
pub fn new(server: &IrcServer<T, U>) -> ServerIterator<T, U> {
@ -501,7 +481,7 @@ mod test {
use std::default::Default;
use std::io::{Cursor, sink};
use client::conn::{Connection, Reconnect};
use client::data::{Config, Message};
use client::data::Config;
#[cfg(not(feature = "nochanlists"))] use client::data::User;
use client::data::command::Command::PRIVMSG;
use client::data::kinds::IrcRead;
@ -538,21 +518,6 @@ mod test {
assert_eq!(&messages[..], exp);
}
#[test]
fn iterator_cmd() {
let exp = "PRIVMSG test :Hi!\r\nPRIVMSG test :This is a test!\r\n\
JOIN #test\r\n";
let server = IrcServer::from_connection(test_config(), Connection::new(
Cursor::new(exp.as_bytes().to_vec()), sink()
));
let mut messages = String::new();
for command in server.iter_cmd() {
let msg: Message = command.unwrap().into();
messages.push_str(&msg.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";