Merge branch 'master' into async

This commit is contained in:
Aaron Weiss 2017-06-19 14:33:02 -04:00
commit cf3ee671ed
No known key found for this signature in database
GPG key ID: 0237035D9BF03AE2
17 changed files with 2145 additions and 1252 deletions

View file

@ -1,7 +1,7 @@
[package]
name = "irc"
version = "0.11.6"
version = "0.11.8"
description = "A simple, thread-safe IRC client library."
authors = ["Aaron Weiss <aaronweiss74@gmail.com>"]
license = "CC0-1.0"
@ -17,12 +17,15 @@ ctcp = []
nochanlists = []
[dependencies]
bufstream = "0.1"
bytes = "0.4"
encoding = "0.2"
futures = "0.1"
native-tls = "0.1"
rustc-serialize = "0.3"
time = "0.1"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
tokio-core = "0.1"
tokio-io = "0.1"
tokio-service = "0.1"

View file

@ -1,4 +1,4 @@
# irc [![Build Status](https://travis-ci.org/aatxe/irc.svg?branch=master)](https://travis-ci.org/aatxe/irc) [![Crates.io](https://img.shields.io/crates/v/irc.svg)](https://crates.io/crates/irc) #
# irc [![Build Status](https://travis-ci.org/aatxe/irc.svg?branch=master)](https://travis-ci.org/aatxe/irc) [![Crates.io](https://img.shields.io/crates/v/irc.svg)](https://crates.io/crates/irc) [![Built with Spacemacs](https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg)](http://spacemacs.org) #
A robust, thread-safe IRC library in Rust. The client portion is compliant with
[RFC 2812](http://tools.ietf.org/html/rfc2812), [IRCv3.1](http://ircv3.net/irc/3.1.html),
[IRCv3.2](http://ircv3.net/irc/3.2.html), and includes some additional, common features. It also
@ -38,7 +38,7 @@ fn main() {
```
It may not seem like much, but all it takes to get started with an IRC connection is the stub
above. In just a few lines, you can be connected to a server and procesisng IRC messages as you
above. In just a few lines, you can be connected to a server and processing IRC messages as you
wish. The library is built with flexibility in mind. If you need to work on multiple threads,
simply clone the server and have at it. We'll take care of the rest.
@ -88,6 +88,8 @@ fail for obvious reasons). That being said, here's an example of a complete conf
},
"umodes": "+RB-x",
"user_info": "I'm a test user for the Rust IRC crate.",
"version": "irc:git:Rust",
"source": "https://github.com/aatxe/irc",
"ping_time": 180,
"ping_timeout": 10,
"options": {

View file

@ -6,17 +6,24 @@ use irc::client::prelude::*;
fn main() {
let config = Config {
nickname: Some(format!("pickles")),
server: Some(format!("irc.fyrechat.net")),
channels: Some(vec![format!("#vana")]),
.. Default::default()
nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#vana".to_owned()]),
..Default::default()
};
let server = IrcServer::from_config(config).unwrap();
server.identify().unwrap();
let server = server.clone();
let _ = spawn(move || {
for msg in server.iter() {
print!("{}", msg.unwrap());
let _ = spawn(move || for message in server.iter() {
let message = message.unwrap(); // We'll just panic if there's an error.
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") {
server.send_privmsg(target, "Hi!").unwrap();
}
}
_ => (),
}
}).join(); // You might not want to join here for actual multi-threading.
}

View file

@ -5,11 +5,11 @@ use irc::client::prelude::*;
fn main() {
let config = Config {
nickname: Some(format!("pickles")),
alt_nicks: Some(vec![format!("bananas"), format!("apples")]),
server: Some(format!("irc.fyrechat.net")),
channels: Some(vec![format!("#vana")]),
.. Default::default()
nickname: Some("pickles".to_owned()),
alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#vana".to_owned()]),
..Default::default()
};
let server = IrcServer::from_config(config).unwrap();
server.identify().unwrap();
@ -17,9 +17,11 @@ fn main() {
let message = message.unwrap(); // We'll just panic if there's an error.
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => if msg.contains("pickles") {
server.send_privmsg(target, "Hi!").unwrap();
},
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") {
server.send_privmsg(target, "Hi!").unwrap();
}
}
_ => (),
}
}

View file

@ -5,12 +5,12 @@ use irc::client::prelude::*;
fn main() {
let config = Config {
nickname: Some(format!("pickles")),
server: Some(format!("irc.fyrechat.net")),
channels: Some(vec![format!("#vana")]),
nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#vana".to_owned()]),
port: Some(6697),
use_ssl: Some(true),
.. Default::default()
..Default::default()
};
let server = IrcServer::from_config(config).unwrap();
server.identify().unwrap();
@ -18,9 +18,11 @@ fn main() {
let message = message.unwrap(); // We'll just panic if there's an error.
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => if msg.contains("pickles") {
server.send_privmsg(target, "Hi!").unwrap();
},
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") {
server.send_privmsg(target, "Hi!").unwrap();
}
}
_ => (),
}
}

View file

@ -7,10 +7,10 @@ use irc::client::prelude::*;
fn main() {
let config = Config {
nickname: Some(format!("pickles")),
server: Some(format!("irc.fyrechat.net")),
channels: Some(vec![format!("#vana")]),
.. Default::default()
nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#vana".to_owned()]),
..Default::default()
};
let server = IrcServer::from_config(config).unwrap();
server.identify().unwrap();

View file

@ -1,8 +1,9 @@
//! Thread-safe connections on IrcStreams.
//! Thread-safe connections on `IrcStreams`.
use std::io::prelude::*;
use std::io::{BufReader, BufWriter, Cursor, Result};
use std::io::{Cursor, Result};
use std::net::TcpStream;
use std::sync::Mutex;
use bufstream::BufStream;
use encoding::DecoderTrap;
use encoding::label::encoding_from_whatwg_label;
@ -24,61 +25,55 @@ pub trait Connection {
/// Useful internal type definitions.
type NetReader = BufReader<NetStream>;
type NetWriter = BufWriter<NetStream>;
type NetReadWritePair = (NetReader, NetWriter);
type NetBufStream = BufStream<NetStream>;
/// A thread-safe connection over a buffered NetStream.
/// A thread-safe connection over a buffered `NetStream`.
pub struct NetConnection {
host: Mutex<String>,
port: Mutex<u16>,
reader: Mutex<NetReader>,
writer: Mutex<NetWriter>,
stream: Mutex<NetBufStream>,
}
impl NetConnection {
fn new(host: &str, port: u16, reader: NetReader, writer: NetWriter) -> NetConnection {
fn new(host: &str, port: u16, stream: NetBufStream) -> NetConnection {
NetConnection {
host: Mutex::new(host.to_owned()),
port: Mutex::new(port),
reader: Mutex::new(reader),
writer: Mutex::new(writer),
stream: Mutex::new(stream),
}
}
/// Creates a thread-safe TCP connection to the specified server.
pub fn connect(host: &str, port: u16) -> Result<NetConnection> {
let (reader, writer) = try!(NetConnection::connect_internal(host, port));
Ok(NetConnection::new(host, port, reader, writer))
let stream = try!(NetConnection::connect_internal(host, port));
Ok(NetConnection::new(host, port, stream))
}
/// connects to the specified server and returns a reader-writer pair.
fn connect_internal(host: &str, port: u16) -> Result<NetReadWritePair> {
let socket = try!(TcpStream::connect(&format!("{}:{}", host, port)[..]));
Ok((BufReader::new(NetStream::Unsecured(try!(socket.try_clone()))),
BufWriter::new(NetStream::Unsecured(socket))))
fn connect_internal(host: &str, port: u16) -> Result<NetBufStream> {
let socket = try!(TcpStream::connect((host, port)));
Ok(BufStream::new(NetStream::Unsecured(socket)))
}
/// Creates a thread-safe TCP connection to the specified server over SSL.
/// If the library is compiled without SSL support, this method panics.
pub fn connect_ssl(host: &str, port: u16) -> Result<NetConnection> {
let (reader, writer) = try!(NetConnection::connect_ssl_internal(host, port));
Ok(NetConnection::new(host, port, reader, writer))
let stream = try!(NetConnection::connect_ssl_internal(host, port));
Ok(NetConnection::new(host, port, stream))
}
/// Panics because SSL support is not compiled in.
fn connect_ssl_internal(host: &str, port: u16) -> Result<NetReadWritePair> {
fn connect_ssl_internal(host: &str, port: u16) -> Result<NetBufStream> {
panic!("Cannot connect to {}:{} over SSL without compiling with SSL support.", host, port)
}
}
impl Connection for NetConnection {
fn send(&self, msg: &str, encoding: &str) -> Result<()> {
imp::send(&self.writer, msg, encoding)
imp::send(&self.stream, msg, encoding)
}
fn recv(&self, encoding: &str) -> Result<String> {
imp::recv(&self.reader, encoding)
imp::recv(&self.stream, encoding)
}
fn written(&self, _: &str) -> Option<String> {
@ -86,18 +81,17 @@ impl Connection for NetConnection {
}
fn reconnect(&self) -> Result<()> {
let use_ssl = match *self.reader.lock().unwrap().get_ref() {
let use_ssl = match *self.stream.lock().unwrap().get_ref() {
NetStream::Unsecured(_) => false,
};
let host = self.host.lock().unwrap();
let port = self.port.lock().unwrap();
let (reader, writer) = if use_ssl {
let stream = if use_ssl {
try!(NetConnection::connect_ssl_internal(&host, *port))
} else {
try!(NetConnection::connect_internal(&host, *port))
};
*self.reader.lock().unwrap() = reader;
*self.writer.lock().unwrap() = writer;
*self.stream.lock().unwrap() = stream;
Ok(())
}
}
@ -138,9 +132,10 @@ impl Connection for MockConnection {
}
fn written(&self, encoding: &str) -> Option<String> {
encoding_from_whatwg_label(encoding).and_then(|enc|
enc.decode(&self.writer.lock().unwrap(), DecoderTrap::Replace).ok()
)
encoding_from_whatwg_label(encoding).and_then(|enc| {
enc.decode(&self.writer.lock().unwrap(), DecoderTrap::Replace)
.ok()
})
}
fn reconnect(&self) -> Result<()> {
@ -160,15 +155,26 @@ mod imp {
pub fn send<T: IrcWrite>(writer: &Mutex<T>, msg: &str, encoding: &str) -> Result<()> {
let encoding = match encoding_from_whatwg_label(encoding) {
Some(enc) => enc,
None => return Err(Error::new(
ErrorKind::InvalidInput, &format!("Failed to find encoder. ({})", encoding)[..]
))
None => {
return Err(Error::new(
ErrorKind::InvalidInput,
&format!("Failed to find encoder. ({})", encoding)[..],
))
}
};
let data = match encoding.encode(msg, EncoderTrap::Replace) {
Ok(data) => data,
Err(data) => return Err(Error::new(ErrorKind::InvalidInput,
&format!("Failed to encode {} as {}.", data, encoding.name())[..]
))
Err(data) => {
return Err(Error::new(
ErrorKind::InvalidInput,
&format!(
"Failed to encode {} as {}.",
data,
encoding.name()
)
[..],
))
}
};
let mut writer = writer.lock().unwrap();
try!(writer.write_all(&data));
@ -178,20 +184,31 @@ mod imp {
pub fn recv<T: IrcRead>(reader: &Mutex<T>, encoding: &str) -> Result<String> {
let encoding = match encoding_from_whatwg_label(encoding) {
Some(enc) => enc,
None => return Err(Error::new(
ErrorKind::InvalidInput, &format!("Failed to find decoder. ({})", encoding)[..]
))
};
let mut buf = Vec::new();
reader.lock().unwrap().read_until(b'\n', &mut buf).and_then(|_|
match encoding.decode(&buf, DecoderTrap::Replace) {
_ if buf.is_empty() => Err(Error::new(ErrorKind::Other, "EOF")),
Ok(data) => Ok(data),
Err(data) => Err(Error::new(ErrorKind::InvalidInput,
&format!("Failed to decode {} as {}.", data, encoding.name())[..]
None => {
return Err(Error::new(
ErrorKind::InvalidInput,
&format!("Failed to find decoder. ({})", encoding)[..],
))
}
)
};
let mut buf = Vec::new();
reader
.lock()
.unwrap()
.read_until(b'\n', &mut buf)
.and_then(|_| match encoding.decode(&buf, DecoderTrap::Replace) {
_ if buf.is_empty() => Err(Error::new(ErrorKind::Other, "EOF")),
Ok(data) => Ok(data),
Err(data) => Err(Error::new(
ErrorKind::InvalidInput,
&format!(
"Failed to decode {} as {}.",
data,
encoding.name()
)
[..],
)),
})
}
}
@ -267,7 +284,13 @@ mod test {
#[cfg(feature = "encode")]
fn send_utf8() {
let conn = MockConnection::empty();
assert!(send_to(&conn, PRIVMSG("test".to_owned(), "€ŠšŽžŒœŸ".to_owned()), "UTF-8").is_ok());
assert!(
send_to(
&conn,
PRIVMSG("test".to_owned(), "€ŠšŽžŒœŸ".to_owned()),
"UTF-8",
).is_ok()
);
let data = conn.written("UTF-8").unwrap();
assert_eq!(&data[..], "PRIVMSG test :€ŠšŽžŒœŸ\r\n");
}
@ -286,7 +309,13 @@ mod test {
#[cfg(feature = "encode")]
fn send_iso885915() {
let conn = MockConnection::empty();
assert!(send_to(&conn, PRIVMSG("test".to_owned(), "€ŠšŽžŒœŸ".to_owned()), "l9").is_ok());
assert!(
send_to(
&conn,
PRIVMSG("test".to_owned(), "€ŠšŽžŒœŸ".to_owned()),
"l9",
).is_ok()
);
let data = conn.written("l9").unwrap();
assert_eq!(&data[..], "PRIVMSG test :€ŠšŽžŒœŸ\r\n");
}
@ -312,7 +341,10 @@ mod test {
#[cfg(feature = "encode")]
fn recv_utf8() {
let conn = MockConnection::new("PRIVMSG test :Testing!\r\n");
assert_eq!(&conn.recv("UTF-8").unwrap()[..], "PRIVMSG test :Testing!\r\n");
assert_eq!(
&conn.recv("UTF-8").unwrap()[..],
"PRIVMSG test :Testing!\r\n"
);
}
#[test]
@ -326,6 +358,9 @@ mod test {
vec.extend("\r\n".as_bytes());
vec.into_iter().collect::<Vec<_>>()
});
assert_eq!(&conn.recv("l9").unwrap()[..], "PRIVMSG test :€ŠšŽžŒœŸ\r\n");
assert_eq!(
&conn.recv("l9").unwrap()[..],
"PRIVMSG test :€ŠšŽžŒœŸ\r\n"
);
}
}

View file

@ -1,4 +1,4 @@
//! JSON configuration files using libserialize.
//! JSON configuration files using serde
use std::borrow::ToOwned;
use std::collections::HashMap;
use std::fs::File;
@ -6,10 +6,10 @@ use std::io::prelude::*;
use std::io::{Error, ErrorKind, Result};
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::Path;
use rustc_serialize::json::{decode, encode};
use serde_json;
/// Configuration data.
#[derive(Clone, RustcDecodable, RustcEncodable, Default, PartialEq, Debug)]
#[derive(Clone, Deserialize, Serialize, Default, PartialEq, Debug)]
pub struct Config {
/// A list of the owners of the client by nickname (for bots).
pub owners: Option<Vec<String>>,
@ -43,6 +43,10 @@ pub struct Config {
pub umodes: Option<String>,
/// The text that'll be sent in response to CTCP USERINFO requests.
pub user_info: Option<String>,
/// The text that'll be sent in response to CTCP VERSION requests.
pub version: Option<String>,
/// The text that'll be sent in response to CTCP SOURCE requests.
pub source: Option<String>,
/// The amount of inactivity in seconds before the client will ping the server.
pub ping_time: Option<u32>,
/// The amount of time in seconds for a client to reconnect due to no ping response.
@ -63,22 +67,33 @@ impl Config {
let mut file = try!(File::open(path));
let mut data = String::new();
try!(file.read_to_string(&mut data));
decode(&data[..]).map_err(|_|
Error::new(ErrorKind::InvalidInput, "Failed to decode configuration file.")
)
serde_json::from_str(&data[..]).map_err(|_| {
Error::new(
ErrorKind::InvalidInput,
"Failed to decode configuration file.",
)
})
}
/// Saves a JSON configuration to the desired path.
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let mut file = try!(File::create(path));
file.write_all(try!(encode(self).map_err(|_|
Error::new(ErrorKind::InvalidInput, "Failed to encode configuration file.")
)).as_bytes())
file.write_all(
try!(serde_json::to_string(self).map_err(|_| {
Error::new(
ErrorKind::InvalidInput,
"Failed to encode configuration file.",
)
})).as_bytes(),
)
}
/// Determines whether or not the nickname provided is the owner of the bot.
pub fn is_owner(&self, nickname: &str) -> bool {
self.owners.as_ref().map(|o| o.contains(&nickname.to_owned())).unwrap()
self.owners
.as_ref()
.map(|o| o.contains(&nickname.to_owned()))
.unwrap()
}
/// Gets the nickname specified in the configuration.
@ -96,7 +111,9 @@ impl Config {
/// Gets the alternate nicknames specified in the configuration.
/// This defaults to an empty vector when not specified.
pub fn alternate_nicknames(&self) -> Vec<&str> {
self.alt_nicks.as_ref().map_or(vec![], |v| v.iter().map(|s| &s[..]).collect())
self.alt_nicks.as_ref().map_or(vec![], |v| {
v.iter().map(|s| &s[..]).collect()
})
}
@ -155,13 +172,17 @@ impl Config {
/// Gets the channels to join upon connection.
/// This defaults to an empty vector if it's not specified.
pub fn channels(&self) -> Vec<&str> {
self.channels.as_ref().map_or(vec![], |v| v.iter().map(|s| &s[..]).collect())
self.channels.as_ref().map_or(vec![], |v| {
v.iter().map(|s| &s[..]).collect()
})
}
/// Gets the key for the specified channel if it exists in the configuration.
pub fn channel_key(&self, chan: &str) -> Option<&str> {
self.channel_keys.as_ref().and_then(|m| m.get(&chan.to_owned()).map(|s| &s[..]))
self.channel_keys.as_ref().and_then(|m| {
m.get(&chan.to_owned()).map(|s| &s[..])
})
}
/// Gets the user modes to set on connect specified in the configuration.
@ -176,6 +197,21 @@ impl Config {
self.user_info.as_ref().map_or("", |s| &s[..])
}
/// Gets the string to be sent in response to CTCP VERSION requests.
/// This defaults to `irc:git:Rust` when not specified.
pub fn version(&self) -> &str {
self.version.as_ref().map_or("irc:git:Rust", |s| &s[..])
}
/// Gets the string to be sent in response to CTCP SOURCE requests.
/// This defaults to `https://github.com/aatxe/irc` when not specified.
pub fn source(&self) -> &str {
self.source.as_ref().map_or(
"https://github.com/aatxe/irc",
|s| &s[..],
)
}
/// Gets the amount of time in seconds since last activity necessary for the client to ping the
/// server.
/// This defaults to 180 seconds when not specified.
@ -198,14 +234,19 @@ impl Config {
/// Gets the NickServ command sequence to recover a nickname.
/// This defaults to `["GHOST"]` when not specified.
pub fn ghost_sequence(&self) -> Vec<&str> {
self.ghost_sequence.as_ref().map_or(vec!["GHOST"], |v| v.iter().map(|s| &s[..]).collect())
self.ghost_sequence.as_ref().map_or(vec!["GHOST"], |v| {
v.iter().map(|s| &s[..]).collect()
})
}
/// Looks up the specified string in the options map.
/// This uses indexing, and thus panics when the string is not present.
/// This will also panic if used and there are no options.
pub fn get_option(&self, option: &str) -> &str {
self.options.as_ref().map(|o| &o[&option.to_owned()][..]).unwrap()
self.options
.as_ref()
.map(|o| &o[&option.to_owned()][..])
.unwrap()
}
}
@ -234,6 +275,8 @@ mod test {
channels: Some(vec![format!("#test"), format!("#test2")]),
channel_keys: None,
user_info: None,
version: None,
source: None,
ping_time: None,
ping_timeout: None,
should_ghost: None,
@ -261,6 +304,8 @@ mod test {
channels: Some(vec![format!("#test"), format!("#test2")]),
channel_keys: None,
user_info: None,
version: None,
source: None,
ping_time: None,
ping_timeout: None,
should_ghost: None,
@ -275,7 +320,7 @@ mod test {
fn is_owner() {
let cfg = Config {
owners: Some(vec![format!("test"), format!("test2")]),
.. Default::default()
..Default::default()
};
assert!(cfg.is_owner("test"));
assert!(cfg.is_owner("test2"));
@ -290,7 +335,7 @@ mod test {
map.insert(format!("testing"), format!("test"));
Some(map)
},
.. Default::default()
..Default::default()
};
assert_eq!(cfg.get_option("testing"), "test");
}

View file

@ -13,11 +13,19 @@ pub mod kinds {
/// Trait describing all possible Writers for this library.
pub trait IrcWrite: Write + Sized + Send + 'static {}
impl<T> IrcWrite for T where T: Write + Sized + Send + 'static {}
impl<T> IrcWrite for T
where
T: Write + Sized + Send + 'static,
{
}
/// Trait describing all possible Readers for this library.
pub trait IrcRead: BufRead + Sized + Send + 'static {}
impl<T> IrcRead for T where T: BufRead + Sized + Send + 'static {}
impl<T> IrcRead for T
where
T: BufRead + Sized + Send + 'static,
{
}
}
pub mod config;

View file

@ -25,9 +25,9 @@ impl User {
let ranks: Vec<_> = AccessLevelIterator::new(string).collect();
let mut state = &string[ranks.len()..];
let nickname = state.find('!').map_or(state, |i| &state[..i]).to_owned();
state = state.find('!').map_or("", |i| &state[i+1..]);
state = state.find('!').map_or("", |i| &state[i + 1..]);
let username = state.find('@').map(|i| state[..i].to_owned());
let hostname = state.find('@').map(|i| state[i+1..].to_owned());
let hostname = state.find('@').map(|i| state[i + 1..].to_owned());
User {
nickname: nickname,
username: username,
@ -39,7 +39,7 @@ impl User {
},
highest_access_level: {
let mut max = AccessLevel::Member;
for rank in ranks.into_iter() {
for rank in ranks {
if rank > max {
max = rank
}
@ -89,8 +89,8 @@ impl User {
"-h" => self.sub_access_level(AccessLevel::HalfOp),
"+v" => self.add_access_level(AccessLevel::Voice),
"-v" => self.sub_access_level(AccessLevel::Voice),
_ => {},
}
_ => {}
}
}
/// Adds an access level to the list, and updates the highest level if necessary.
@ -123,7 +123,7 @@ impl User {
impl PartialEq for User {
fn eq(&self, other: &User) -> bool {
self.nickname == other.nickname && self.username == other.username &&
self.hostname == other.hostname
self.hostname == other.hostname
}
}
@ -146,7 +146,9 @@ pub enum AccessLevel {
impl PartialOrd for AccessLevel {
fn partial_cmp(&self, other: &AccessLevel) -> Option<Ordering> {
if self == other { return Some(Equal) }
if self == other {
return Some(Equal);
}
match *self {
AccessLevel::Owner => Some(Greater),
AccessLevel::Admin => {
@ -155,28 +157,28 @@ impl PartialOrd for AccessLevel {
} else {
Some(Greater)
}
},
}
AccessLevel::Oper => {
if other == &AccessLevel::Owner || other == &AccessLevel::Admin {
Some(Less)
} else {
Some(Greater)
}
},
}
AccessLevel::HalfOp => {
if other == &AccessLevel::Voice || other == &AccessLevel::Member {
Some(Greater)
} else {
Some(Less)
}
},
}
AccessLevel::Voice => {
if other == &AccessLevel::Member {
Some(Greater)
} else {
Some(Less)
}
},
}
AccessLevel::Member => Some(Less),
}
}
@ -192,7 +194,7 @@ impl FromStr for AccessLevel {
Some('%') => Ok(AccessLevel::HalfOp),
Some('+') => Ok(AccessLevel::Voice),
None => Err("No access level in an empty string."),
_ => Err("Failed to parse access level."),
_ => Err("Failed to parse access level."),
}
}
}
@ -258,7 +260,7 @@ mod test {
username: None,
hostname: None,
highest_access_level: Owner,
access_levels: vec![Owner, Admin, Voice, Member]
access_levels: vec![Owner, Admin, Voice, Member],
};
assert_eq!(user, exp);
assert_eq!(user.highest_access_level, exp.highest_access_level);
@ -324,7 +326,10 @@ mod test {
fn derank_user_in_full() {
let mut user = User::new("~&@%+user");
assert_eq!(user.highest_access_level, Owner);
assert_eq!(user.access_levels, vec![Owner, Admin, Oper, HalfOp, Voice, Member]);
assert_eq!(
user.access_levels,
vec![Owner, Admin, Oper, HalfOp, Voice, Member]
);
user.update_access_level("-h");
assert_eq!(user.highest_access_level, Owner);
assert_eq!(user.access_levels, vec![Owner, Admin, Oper, Member, Voice]);

View file

@ -6,11 +6,12 @@ use std::error::Error as StdError;
use std::io::{Error, ErrorKind, Result};
use std::path::Path;
use std::sync::{Arc, Mutex, RwLock};
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{spawn, sleep};
use std::time::Duration as StdDuration;
use client::conn::{Connection, NetConnection};
use client::data::{Command, Config, Message, Response, User};
use client::data::Command::{JOIN, NICK, NICKSERV, PART, PING, PONG, PRIVMSG, MODE};
use client::data::Command::{JOIN, NICK, NICKSERV, PART, PING, PONG, PRIVMSG, MODE, QUIT};
use client::server::utils::ServerExt;
use time::{Duration, Timespec, Tm, now};
@ -22,11 +23,16 @@ pub trait Server {
fn config(&self) -> &Config;
/// Sends a Command to this Server.
fn send<M: Into<Message>>(&self, message: M) -> Result<()> where Self: Sized;
fn send<M: Into<Message>>(&self, message: M) -> Result<()>
where
Self: Sized;
/// Gets an iterator over received messages.
fn iter<'a>(&'a self) -> Box<Iterator<Item = Result<Message>> + 'a>;
/// Gets a list of currently joined channels. This will be none if tracking is not supported altogether.
fn list_channels(&self) -> Option<Vec<String>>;
/// 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.
@ -58,11 +64,14 @@ struct ServerState {
/// A thread-safe store for the last ping data.
last_ping_data: Mutex<Option<Timespec>>,
/// A thread-safe check of pong reply.
waiting_pong_reply: Mutex<bool>,
waiting_pong_reply: AtomicBool,
}
impl ServerState {
fn new<C>(conn: C, config: Config) -> ServerState where C: Connection + Send + Sync + 'static {
fn new<C>(conn: C, config: Config) -> ServerState
where
C: Connection + Send + Sync + 'static,
{
ServerState {
conn: Box::new(conn),
config: config,
@ -71,15 +80,14 @@ impl ServerState {
reconnect_count: Mutex::new(0),
last_action_time: Mutex::new(now()),
last_ping_data: Mutex::new(None),
waiting_pong_reply: Mutex::new(false),
waiting_pong_reply: AtomicBool::new(false),
}
}
fn reconnect(&self) -> Result<()> {
let mut ping_data = self.last_ping_data.lock().unwrap();
*ping_data = None;
let mut waiting_pong_reply = self.waiting_pong_reply.lock().unwrap();
*waiting_pong_reply = false;
self.waiting_pong_reply.store(false, Ordering::SeqCst);
self.conn.reconnect()
}
@ -100,8 +108,7 @@ impl ServerState {
fn update_ping_data(&self, data: Timespec) {
let mut ping_data = self.last_ping_data.lock().unwrap();
*ping_data = Some(data);
let mut waiting_pong_reply = self.waiting_pong_reply.lock().unwrap();
*waiting_pong_reply = true;
self.waiting_pong_reply.store(true, Ordering::SeqCst);
}
fn last_ping_data(&self) -> Option<Timespec> {
@ -109,7 +116,7 @@ impl ServerState {
}
fn waiting_pong_reply(&self) -> bool {
*self.waiting_pong_reply.lock().unwrap()
self.waiting_pong_reply.load(Ordering::SeqCst)
}
fn check_pong(&self, data: &str) {
@ -117,8 +124,7 @@ impl ServerState {
let fmt = format!("{}", time.sec);
if fmt == data {
// found matching pong reply
let mut waiting_reply = self.waiting_pong_reply.lock().unwrap();
*waiting_reply = false;
self.waiting_pong_reply.store(false, Ordering::SeqCst);
}
}
}
@ -130,8 +136,26 @@ impl ServerState {
self.action_taken();
}
/// Sanitizes the input string by cutting up to (and including) the first occurence of a line
/// terminiating phrase (`\r\n`, `\r`, or `\n`).
fn sanitize(data: &str) -> &str {
// n.b. ordering matters here to prefer "\r\n" over "\r"
if let Some((pos, len)) = ["\r\n", "\r", "\n"]
.iter()
.flat_map(|needle| data.find(needle).map(|pos| (pos, needle.len())))
.min_by_key(|&(pos, _)| pos)
{
data.split_at(pos + len).0
} else {
data
}
}
fn write<M: Into<Message>>(&self, msg: M) -> Result<()> {
self.conn.send(&msg.into().to_string(), self.config.encoding())
self.conn.send(
ServerState::sanitize(&msg.into().to_string()),
self.config.encoding(),
)
}
}
@ -158,7 +182,7 @@ impl Clone for IrcServer {
fn clone(&self) -> IrcServer {
IrcServer {
state: self.state.clone(),
reconnect_count: self.reconnect_count.clone()
reconnect_count: self.reconnect_count.clone(),
}
}
}
@ -168,7 +192,10 @@ impl<'a> Server for ServerState {
&self.config
}
fn send<M: Into<Message>>(&self, msg: M) -> Result<()> where Self: Sized {
fn send<M: Into<Message>>(&self, msg: M) -> Result<()>
where
Self: Sized,
{
self.send_impl(msg.into());
Ok(())
}
@ -178,10 +205,30 @@ impl<'a> Server for ServerState {
}
#[cfg(not(feature = "nochanlists"))]
fn list_users(&self, chan: &str) -> Option<Vec<User>> {
self.chanlists.lock().unwrap().get(&chan.to_owned()).cloned()
fn list_channels(&self) -> Option<Vec<String>> {
Some(
self.chanlists
.lock()
.unwrap()
.keys()
.map(|k| k.to_owned())
.collect(),
)
}
#[cfg(feature = "nochanlists")]
fn list_channels(&self) -> Option<Vec<String>> {
None
}
#[cfg(not(feature = "nochanlists"))]
fn list_users(&self, chan: &str) -> Option<Vec<User>> {
self.chanlists
.lock()
.unwrap()
.get(&chan.to_owned())
.cloned()
}
#[cfg(feature = "nochanlists")]
fn list_users(&self, _: &str) -> Option<Vec<User>> {
@ -194,7 +241,12 @@ impl Server for IrcServer {
&self.state.config
}
fn send<M: Into<Message>>(&self, msg: M) -> Result<()> where Self: Sized {
fn send<M: Into<Message>>(&self, msg: M) -> Result<()>
where
Self: Sized,
{
let msg = msg.into();
try!(self.handle_sent_message(&msg));
self.state.send(msg)
}
@ -203,10 +255,32 @@ impl Server for IrcServer {
}
#[cfg(not(feature = "nochanlists"))]
fn list_users(&self, chan: &str) -> Option<Vec<User>> {
self.state.chanlists.lock().unwrap().get(&chan.to_owned()).cloned()
fn list_channels(&self) -> Option<Vec<String>> {
Some(
self.state
.chanlists
.lock()
.unwrap()
.keys()
.map(|k| k.to_owned())
.collect(),
)
}
#[cfg(feature = "nochanlists")]
fn list_channels(&self) -> Option<Vec<String>> {
None
}
#[cfg(not(feature = "nochanlists"))]
fn list_users(&self, chan: &str) -> Option<Vec<User>> {
self.state
.chanlists
.lock()
.unwrap()
.get(&chan.to_owned())
.cloned()
}
#[cfg(feature = "nochanlists")]
fn list_users(&self, _: &str) -> Option<Vec<User>> {
@ -217,7 +291,9 @@ impl Server for IrcServer {
impl IrcServer {
/// Creates an IRC server from the specified configuration, and any arbitrary sync connection.
pub fn from_connection<C>(config: Config, conn: C) -> IrcServer
where C: Connection + Send + Sync + 'static {
where
C: Connection + Send + Sync + 'static,
{
let state = Arc::new(ServerState::new(conn, config));
let ping_time = (state.config.ping_time() as i64) * 1000;
let ping_timeout = (state.config.ping_timeout() as i64) * 1000;
@ -237,7 +313,8 @@ impl IrcServer {
}
};
let now_timespec = now().to_timespec();
let sleep_dur_ping_time = ping_time - (now_timespec - ping_idle_timespec).num_milliseconds();
let sleep_dur_ping_time = ping_time -
(now_timespec - ping_idle_timespec).num_milliseconds();
let sleep_dur_ping_timeout = if let Some(time) = strong.last_ping_data() {
ping_timeout - (now_timespec - time).num_milliseconds()
} else {
@ -272,7 +349,10 @@ impl IrcServer {
}
}
});
IrcServer { state: state, reconnect_count: Cell::new(0) }
IrcServer {
state: state,
reconnect_count: Cell::new(0),
}
}
/// Gets a reference to the IRC server's connection.
@ -299,7 +379,7 @@ impl IrcServer {
let index = self.state.alt_nick_index.read().unwrap();
match *index {
0 => self.config().nickname(),
i => alt_nicks[i - 1]
i => alt_nicks[i - 1],
}
}
@ -308,50 +388,70 @@ impl IrcServer {
&self.state.chanlists
}
/// Handles messages internally for basic client functionality.
/// Handles sent messages internally for basic client functionality.
fn handle_sent_message(&self, msg: &Message) -> Result<()> {
match msg.command {
PART(ref chan, _) => {
let _ = self.state.chanlists.lock().unwrap().remove(chan);
}
_ => (),
}
Ok(())
}
/// Handles received messages internally for basic client functionality.
fn handle_message(&self, msg: &Message) -> Result<()> {
match msg.command {
PING(ref data, _) => try!(self.send_pong(&data)),
PONG(_, Some(ref pingdata)) => self.state.check_pong(&pingdata),
PONG(ref pingdata, None) => self.state.check_pong(&pingdata),
PING(ref data, _) => try!(self.send_pong(data)),
PONG(ref pingdata, None) |
PONG(_, Some(ref pingdata)) => self.state.check_pong(pingdata),
JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan),
PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan),
QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")),
NICK(ref new_nick) => {
self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick)
}
MODE(ref chan, ref mode, Some(ref user)) => self.handle_mode(chan, mode, user),
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()
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()
};
body[1..end].split(' ').collect()
};
if target.starts_with('#') {
try!(self.handle_ctcp(&target, tokens))
} else if let Some(user) = msg.source_nickname() {
try!(self.handle_ctcp(user, tokens))
if target.starts_with('#') {
try!(self.handle_ctcp(target, tokens))
} else if let Some(user) = msg.source_nickname() {
try!(self.handle_ctcp(user, tokens))
}
}
},
}
Command::Response(Response::RPL_NAMREPLY, ref args, ref suffix) => {
self.handle_namreply(args, suffix)
},
}
Command::Response(Response::RPL_ENDOFMOTD, _, _) |
Command::Response(Response::ERR_NOMOTD, _, _) => {
try!(self.send_nick_password());
try!(self.send_umodes());
let config_chans = self.config().channels();
for chan in config_chans.iter() {
for chan in &config_chans {
match self.config().channel_key(chan) {
Some(key) => try!(self.send_join_with_keys(chan, key)),
None => try!(self.send_join(chan))
None => try!(self.send_join(chan)),
}
}
let joined_chans = self.state.chanlists.lock().unwrap();
for chan in joined_chans.keys().filter(|x| !config_chans.contains(&x.as_str())) {
for chan in joined_chans.keys().filter(
|x| !config_chans.contains(&x.as_str()),
)
{
try!(self.send_join(chan))
}
},
}
Command::Response(Response::ERR_NICKNAMEINUSE, _, _) |
Command::Response(Response::ERR_ERRONEOUSNICKNAME, _, _) => {
let alt_nicks = self.config().alternate_nicknames();
@ -362,8 +462,8 @@ impl IrcServer {
try!(self.send(NICK(alt_nicks[*index].to_owned())));
*index += 1;
}
},
_ => ()
}
_ => (),
}
Ok(())
}
@ -376,14 +476,18 @@ impl IrcServer {
if self.config().should_ghost() && *index != 0 {
for seq in &self.config().ghost_sequence() {
try!(self.send(NICKSERV(format!(
"{} {} {}", seq, self.config().nickname(),
"{} {} {}",
seq,
self.config().nickname(),
self.config().nick_password()
))));
}
*index = 0;
try!(self.send(NICK(self.config().nickname().to_owned())))
}
self.send(NICKSERV(format!("IDENTIFY {}", self.config().nick_password())))
self.send(NICKSERV(
format!("IDENTIFY {}", self.config().nick_password()),
))
}
}
@ -421,6 +525,43 @@ impl IrcServer {
}
}
#[cfg(feature = "nochanlists")]
fn handle_quit(&self, _: &str) {}
#[cfg(not(feature = "nochanlists"))]
fn handle_quit(&self, src: &str) {
if src.is_empty() {
return;
}
let mut chanlists = self.chanlists().lock().unwrap();
for channel in chanlists.clone().keys() {
if let Some(vec) = chanlists.get_mut(&channel.to_owned()) {
if let Some(p) = vec.iter().position(|x| x.get_nickname() == src) {
vec.swap_remove(p);
}
}
}
}
#[cfg(feature = "nochanlists")]
fn handle_nick_change(&self, _: &str, _: &str) {}
#[cfg(not(feature = "nochanlists"))]
fn handle_nick_change(&self, old_nick: &str, new_nick: &str) {
if old_nick.is_empty() || new_nick.is_empty() {
return;
}
let mut chanlists = self.chanlists().lock().unwrap();
for channel in chanlists.clone().keys() {
if let Some(vec) = chanlists.get_mut(&channel.to_owned()) {
if let Some(n) = vec.iter().position(|x| x.get_nickname() == old_nick) {
let new_entry = User::new(new_nick);
vec[n] = new_entry;
}
}
}
}
#[cfg(feature = "nochanlists")]
fn handle_mode(&self, chan: &str, mode: &str, user: &str) {}
@ -428,7 +569,7 @@ impl IrcServer {
fn handle_mode(&self, chan: &str, mode: &str, user: &str) {
if let Some(vec) = self.chanlists().lock().unwrap().get_mut(chan) {
if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) {
vec[n].update_access_level(&mode)
vec[n].update_access_level(mode)
}
}
}
@ -443,8 +584,10 @@ impl IrcServer {
let chan = &args[2];
for user in users.split(' ') {
let mut chanlists = self.state.chanlists.lock().unwrap();
chanlists.entry(chan.clone()).or_insert_with(Vec::new)
.push(User::new(user))
chanlists
.entry(chan.clone())
.or_insert_with(Vec::new)
.push(User::new(user))
}
}
}
@ -453,26 +596,38 @@ impl IrcServer {
/// Handles CTCP requests if the CTCP feature is enabled.
#[cfg(feature = "ctcp")]
fn handle_ctcp(&self, resp: &str, tokens: Vec<&str>) -> Result<()> {
if tokens.len() == 0 { return Ok(()) }
if tokens.is_empty() {
return Ok(());
}
match tokens[0] {
"FINGER" => self.send_ctcp_internal(resp, &format!(
"FINGER :{} ({})", self.config().real_name(), self.config().username()
)),
"VERSION" => self.send_ctcp_internal(resp, "VERSION irc:git:Rust"),
"FINGER" => {
self.send_ctcp_internal(
resp,
&format!(
"FINGER :{} ({})",
self.config().real_name(),
self.config().username()
),
)
}
"VERSION" => {
self.send_ctcp_internal(resp, &format!("VERSION {}", self.config().version()))
}
"SOURCE" => {
try!(self.send_ctcp_internal(resp, "SOURCE https://github.com/aatxe/irc"));
try!(self.send_ctcp_internal(
resp,
&format!("SOURCE {}", self.config().source()),
));
self.send_ctcp_internal(resp, "SOURCE")
},
}
"PING" if tokens.len() > 1 => {
self.send_ctcp_internal(resp, &format!("PING {}", tokens[1]))
},
"TIME" => self.send_ctcp_internal(resp, &format!(
"TIME :{}", now().rfc822z()
)),
"USERINFO" => self.send_ctcp_internal(resp, &format!(
"USERINFO :{}", self.config().user_info()
)),
_ => Ok(())
}
"TIME" => self.send_ctcp_internal(resp, &format!("TIME :{}", now().rfc822z())),
"USERINFO" => {
self.send_ctcp_internal(resp, &format!("USERINFO :{}", self.config().user_info()))
}
_ => Ok(()),
}
}
@ -489,9 +644,9 @@ impl IrcServer {
}
}
/// An Iterator over an IrcServer's incoming Messages.
/// An `Iterator` over an `IrcServer`'s incoming `Messages`.
pub struct ServerIterator<'a> {
server: &'a IrcServer
server: &'a IrcServer,
}
impl<'a> ServerIterator<'a> {
@ -511,19 +666,24 @@ impl<'a> Iterator for ServerIterator<'a> {
fn next(&mut self) -> Option<Result<Message>> {
loop {
match self.get_next_line() {
Ok(msg) => match msg.parse() {
Ok(res) => {
match self.server.handle_message(&res) {
Ok(()) => (),
Err(err) => return Some(Err(err))
Ok(msg) => {
match msg.parse() {
Ok(res) => {
match self.server.handle_message(&res) {
Ok(()) => (),
Err(err) => return Some(Err(err)),
}
self.server.state.action_taken();
return Some(Ok(res));
}
self.server.state.action_taken();
return Some(Ok(res))
},
Err(_) => return Some(Err(Error::new(ErrorKind::InvalidInput,
&format!("Failed to parse message. (Message: {})", msg)[..]
)))
},
Err(_) => {
return Some(Err(Error::new(
ErrorKind::InvalidInput,
&format!("Failed to parse message. (Message: {})", msg)[..],
)))
}
}
}
Err(ref err) if err.description() == "EOF" => return None,
Err(_) => {
let _ = self.server.reconnect().and_then(|_| self.server.identify());
@ -540,8 +700,9 @@ mod test {
use std::default::Default;
use client::conn::MockConnection;
use client::data::Config;
#[cfg(not(feature = "nochanlists"))] use client::data::User;
use client::data::command::Command::PRIVMSG;
#[cfg(not(feature = "nochanlists"))]
use client::data::User;
use client::data::command::Command::{PART, PRIVMSG};
pub fn test_config() -> Config {
Config {
@ -551,7 +712,7 @@ mod test {
server: Some(format!("irc.test.net")),
channels: Some(vec![format!("#test"), format!("#test2")]),
user_info: Some(format!("Testing.")),
.. Default::default()
..Default::default()
}
}
@ -584,98 +745,129 @@ mod test {
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");
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()
}, MockConnection::new(value));
let server = IrcServer::from_connection(
Config {
nick_password: Some(format!("password")),
channels: Some(vec![format!("#test"), format!("#test2")]),
..Default::default()
},
MockConnection::new(value),
);
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(&get_server_value(server)[..], "NICKSERV IDENTIFY password\r\nJOIN #test\r\n\
JOIN #test2\r\n");
assert_eq!(
&get_server_value(server)[..],
"NICKSERV IDENTIFY password\r\nJOIN #test\r\n\
JOIN #test2\r\n"
);
}
#[test]
fn handle_end_motd_with_chan_keys() {
let value = ":irc.test.net 376 test :End of /MOTD command\r\n";
let server = IrcServer::from_connection(Config {
nickname: Some(format!("test")),
channels: Some(vec![format!("#test"), format!("#test2")]),
channel_keys: {
let mut map = HashMap::new();
map.insert(format!("#test2"), format!("password"));
Some(map)
let server = IrcServer::from_connection(
Config {
nickname: Some(format!("test")),
channels: Some(vec![format!("#test"), format!("#test2")]),
channel_keys: {
let mut map = HashMap::new();
map.insert(format!("#test2"), format!("password"));
Some(map)
},
..Default::default()
},
.. Default::default()
}, MockConnection::new(value));
MockConnection::new(value),
);
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(&get_server_value(server)[..], "JOIN #test\r\nJOIN #test2 password\r\n");
assert_eq!(
&get_server_value(server)[..],
"JOIN #test\r\nJOIN #test2 password\r\n"
);
}
#[test]
fn handle_end_motd_with_ghost() {
let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\
:irc.test.net 376 test2 :End of /MOTD command.\r\n";
let server = IrcServer::from_connection(Config {
nickname: Some(format!("test")),
alt_nicks: Some(vec![format!("test2")]),
nick_password: Some(format!("password")),
channels: Some(vec![format!("#test"), format!("#test2")]),
should_ghost: Some(true),
.. Default::default()
}, MockConnection::new(value));
let server = IrcServer::from_connection(
Config {
nickname: Some(format!("test")),
alt_nicks: Some(vec![format!("test2")]),
nick_password: Some(format!("password")),
channels: Some(vec![format!("#test"), format!("#test2")]),
should_ghost: Some(true),
..Default::default()
},
MockConnection::new(value),
);
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(&get_server_value(server)[..], "NICK :test2\r\nNICKSERV GHOST test password\r\n\
NICK :test\r\nNICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n");
assert_eq!(
&get_server_value(server)[..],
"NICK :test2\r\nNICKSERV GHOST test password\r\n\
NICK :test\r\nNICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n"
);
}
#[test]
fn handle_end_motd_with_ghost_seq() {
let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\
:irc.test.net 376 test2 :End of /MOTD command.\r\n";
let server = IrcServer::from_connection(Config {
nickname: Some(format!("test")),
alt_nicks: Some(vec![format!("test2")]),
nick_password: Some(format!("password")),
channels: Some(vec![format!("#test"), format!("#test2")]),
should_ghost: Some(true),
ghost_sequence: Some(vec![format!("RECOVER"), format!("RELEASE")]),
.. Default::default()
}, MockConnection::new(value));
let server = IrcServer::from_connection(
Config {
nickname: Some(format!("test")),
alt_nicks: Some(vec![format!("test2")]),
nick_password: Some(format!("password")),
channels: Some(vec![format!("#test"), format!("#test2")]),
should_ghost: Some(true),
ghost_sequence: Some(vec![format!("RECOVER"), format!("RELEASE")]),
..Default::default()
},
MockConnection::new(value),
);
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(&get_server_value(server)[..], "NICK :test2\r\nNICKSERV RECOVER test password\
assert_eq!(
&get_server_value(server)[..],
"NICK :test2\r\nNICKSERV RECOVER test password\
\r\nNICKSERV RELEASE test password\r\nNICK :test\r\nNICKSERV IDENTIFY password\
\r\nJOIN #test\r\nJOIN #test2\r\n");
\r\nJOIN #test\r\nJOIN #test2\r\n"
);
}
#[test]
fn handle_end_motd_with_umodes() {
let value = ":irc.test.net 376 test :End of /MOTD command.\r\n";
let server = IrcServer::from_connection(Config {
nickname: Some(format!("test")),
umodes: Some(format!("+B")),
channels: Some(vec![format!("#test"), format!("#test2")]),
.. Default::default()
}, MockConnection::new(value));
let server = IrcServer::from_connection(
Config {
nickname: Some(format!("test")),
umodes: Some(format!("+B")),
channels: Some(vec![format!("#test"), format!("#test2")]),
..Default::default()
},
MockConnection::new(value),
);
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(&get_server_value(server)[..],
"MODE test +B\r\nJOIN #test\r\nJOIN #test2\r\n");
assert_eq!(
&get_server_value(server)[..],
"MODE test +B\r\nJOIN #test\r\nJOIN #test2\r\n"
);
}
#[test]
@ -702,8 +894,49 @@ mod test {
#[test]
fn send() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
assert!(server.send(PRIVMSG(format!("#test"), format!("Hi there!"))).is_ok());
assert_eq!(&get_server_value(server)[..], "PRIVMSG #test :Hi there!\r\n");
assert!(
server
.send(PRIVMSG(format!("#test"), format!("Hi there!")))
.is_ok()
);
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG #test :Hi there!\r\n"
);
}
#[test]
fn send_no_newline_injection() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
assert!(
server
.send(PRIVMSG(format!("#test"), format!("Hi there!\nJOIN #bad")))
.is_ok()
);
assert_eq!(&get_server_value(server)[..], "PRIVMSG #test :Hi there!\n");
}
#[test]
#[cfg(not(feature = "nochanlists"))]
fn channel_tracking_names() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n";
let server = IrcServer::from_connection(test_config(), MockConnection::new(value));
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(server.list_channels().unwrap(), vec!["#test".to_owned()])
}
#[test]
#[cfg(not(feature = "nochanlists"))]
fn channel_tracking_names_part() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n";
let server = IrcServer::from_connection(test_config(), MockConnection::new(value));
for message in server.iter() {
println!("{:?}", message);
}
assert!(server.send(PART(format!("#test"), None)).is_ok());
assert!(server.list_channels().unwrap().is_empty())
}
#[test]
@ -714,8 +947,10 @@ mod test {
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(server.list_users("#test").unwrap(),
vec![User::new("test"), User::new("~owner"), User::new("&admin")])
assert_eq!(
server.list_users("#test").unwrap(),
vec![User::new("test"), User::new("~owner"), User::new("&admin")]
)
}
#[test]
@ -727,8 +962,15 @@ mod test {
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")])
assert_eq!(
server.list_users("#test").unwrap(),
vec![
User::new("test"),
User::new("~owner"),
User::new("&admin"),
User::new("test2"),
]
)
}
#[test]
@ -740,8 +982,10 @@ mod test {
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(server.list_users("#test").unwrap(),
vec![User::new("test"), User::new("&admin")])
assert_eq!(
server.list_users("#test").unwrap(),
vec![User::new("test"), User::new("&admin")]
)
}
#[test]
@ -753,12 +997,16 @@ mod test {
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(server.list_users("#test").unwrap(),
vec![User::new("@test"), User::new("~owner"), User::new("&admin")]);
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());
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();
@ -785,8 +1033,11 @@ mod test {
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(&get_server_value(server)[..], "NOTICE test :\u{001}FINGER :test (test)\u{001}\
\r\n");
assert_eq!(
&get_server_value(server)[..],
"NOTICE test :\u{001}FINGER :test (test)\u{001}\
\r\n"
);
}
#[test]
@ -797,8 +1048,11 @@ mod test {
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");
assert_eq!(
&get_server_value(server)[..],
"NOTICE test :\u{001}VERSION irc:git:Rust\u{001}\
\r\n"
);
}
#[test]
@ -809,9 +1063,11 @@ mod test {
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");
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]
@ -822,7 +1078,10 @@ mod test {
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(&get_server_value(server)[..], "NOTICE test :\u{001}PING test\u{001}\r\n");
assert_eq!(
&get_server_value(server)[..],
"NOTICE test :\u{001}PING test\u{001}\r\n"
);
}
#[test]
@ -846,8 +1105,11 @@ mod test {
for message in server.iter() {
println!("{:?}", message);
}
assert_eq!(&get_server_value(server)[..], "NOTICE test :\u{001}USERINFO :Testing.\u{001}\
\r\n");
assert_eq!(
&get_server_value(server)[..],
"NOTICE test :\u{001}USERINFO :Testing.\u{001}\
\r\n"
);
}
#[test]

View file

@ -5,89 +5,143 @@ use client::data::{Capability, NegotiationVersion};
use client::data::Command::{AUTHENTICATE, CAP, INVITE, JOIN, KICK, KILL, MODE, NICK, NOTICE};
use client::data::Command::{OPER, PART, PASS, PONG, PRIVMSG, QUIT, SAMODE, SANICK, TOPIC, USER};
use proto::command::CapSubCommand::{END, LS, REQ};
#[cfg(feature = "ctcp")] use time::get_time;
use client::server::Server;
use time;
/// Extensions for Server capabilities that make it easier to work directly with the protocol.
pub trait ServerExt: Server {
/// Sends a request for a list of server capabilities for a specific IRCv3 version.
fn send_cap_ls(&self, version: NegotiationVersion) -> Result<()> where Self: Sized {
self.send(CAP(None, LS, match version {
NegotiationVersion::V301 => None,
NegotiationVersion::V302 => Some("302".to_owned()),
}, None))
fn send_cap_ls(&self, version: NegotiationVersion) -> Result<()>
where
Self: Sized,
{
self.send(CAP(
None,
LS,
match version {
NegotiationVersion::V301 => None,
NegotiationVersion::V302 => Some("302".to_owned()),
},
None,
))
}
/// Sends an IRCv3 capabilities request for the specified extensions.
fn send_cap_req(&self, extensions: &[Capability]) -> Result<()> where Self: Sized {
let append = |mut s: String, c| { s.push_str(c); s.push(' '); s };
let mut exts = extensions.iter().map(|c| c.as_ref()).fold(String::new(), append);
fn send_cap_req(&self, extensions: &[Capability]) -> Result<()>
where
Self: Sized,
{
let append = |mut s: String, c| {
s.push_str(c);
s.push(' ');
s
};
let mut exts = extensions.iter().map(|c| c.as_ref()).fold(
String::new(),
append,
);
let len = exts.len() - 1;
exts.truncate(len);
self.send(CAP(None, REQ, None, Some(exts)))
}
/// Sends a CAP END, NICK and USER to identify.
fn identify(&self) -> Result<()> where Self: Sized {
fn identify(&self) -> Result<()>
where
Self: Sized,
{
// Send a CAP END to signify that we're IRCv3-compliant (and to end negotiations!).
try!(self.send(CAP(None, END, None, None)));
if self.config().password() != "" {
try!(self.send(PASS(self.config().password().to_owned())));
}
try!(self.send(NICK(self.config().nickname().to_owned())));
try!(self.send(USER(self.config().username().to_owned(), "0".to_owned(),
self.config().real_name().to_owned())));
try!(self.send(USER(
self.config().username().to_owned(),
"0".to_owned(),
self.config().real_name().to_owned(),
)));
Ok(())
}
/// Sends a SASL AUTHENTICATE message with the specified data.
fn send_sasl(&self, data: &str) -> Result<()> where Self: Sized {
fn send_sasl(&self, data: &str) -> Result<()>
where
Self: Sized,
{
self.send(AUTHENTICATE(data.to_owned()))
}
/// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism.
fn send_sasl_plain(&self) -> Result<()> where Self: Sized {
fn send_sasl_plain(&self) -> Result<()>
where
Self: Sized,
{
self.send_sasl("PLAIN")
}
/// Sends a SASL AUTHENTICATE request to use the EXTERNAL mechanism.
fn send_sasl_external(&self) -> Result<()> where Self: Sized {
fn send_sasl_external(&self) -> Result<()>
where
Self: Sized,
{
self.send_sasl("EXTERNAL")
}
/// Sends a SASL AUTHENTICATE request to abort authentication.
fn send_sasl_abort(&self) -> Result<()> where Self: Sized {
fn send_sasl_abort(&self) -> Result<()>
where
Self: Sized,
{
self.send_sasl("*")
}
/// Sends a PONG with the specified message.
fn send_pong(&self, msg: &str) -> Result<()> where Self: Sized {
fn send_pong(&self, msg: &str) -> Result<()>
where
Self: Sized,
{
self.send(PONG(msg.to_owned(), None))
}
/// Joins the specified channel or chanlist.
fn send_join(&self, chanlist: &str) -> Result<()> where Self: Sized {
fn send_join(&self, chanlist: &str) -> Result<()>
where
Self: Sized,
{
self.send(JOIN(chanlist.to_owned(), None, None))
}
/// Joins the specified channel or chanlist using the specified key or keylist.
fn send_join_with_keys(&self, chanlist: &str, keylist: &str) -> Result<()> where Self: Sized {
fn send_join_with_keys(&self, chanlist: &str, keylist: &str) -> Result<()>
where
Self: Sized,
{
self.send(JOIN(chanlist.to_owned(), Some(keylist.to_owned()), None))
}
/// Parts the specified channel or chanlist.
fn send_part(&self, chanlist: &str) -> Result<()> where Self: Sized {
fn send_part(&self, chanlist: &str) -> Result<()>
where
Self: Sized,
{
self.send(PART(chanlist.to_owned(), None))
}
/// Attempts to oper up using the specified username and password.
fn send_oper(&self, username: &str, password: &str) -> Result<()> where Self: Sized {
fn send_oper(&self, username: &str, password: &str) -> Result<()>
where
Self: Sized,
{
self.send(OPER(username.to_owned(), password.to_owned()))
}
/// Sends a message to the specified target.
fn send_privmsg(&self, target: &str, message: &str) -> Result<()> where Self: Sized {
fn send_privmsg(&self, target: &str, message: &str) -> Result<()>
where
Self: Sized,
{
for line in message.split("\r\n") {
try!(self.send(PRIVMSG(target.to_owned(), line.to_owned())))
}
@ -95,7 +149,10 @@ pub trait ServerExt: Server {
}
/// Sends a notice to the specified target.
fn send_notice(&self, target: &str, message: &str) -> Result<()> where Self: Sized {
fn send_notice(&self, target: &str, message: &str) -> Result<()>
where
Self: Sized,
{
for line in message.split("\r\n") {
try!(self.send(NOTICE(target.to_owned(), line.to_owned())))
}
@ -104,65 +161,101 @@ pub trait ServerExt: Server {
/// Sets the topic of a channel or requests the current one.
/// If `topic` is an empty string, it won't be included in the message.
fn send_topic(&self, channel: &str, topic: &str) -> Result<()> where Self: Sized {
self.send(TOPIC(channel.to_owned(), if topic.is_empty() {
None
} else {
Some(topic.to_owned())
}))
fn send_topic(&self, channel: &str, topic: &str) -> Result<()>
where
Self: Sized,
{
self.send(TOPIC(
channel.to_owned(),
if topic.is_empty() {
None
} else {
Some(topic.to_owned())
},
))
}
/// Kills the target with the provided message.
fn send_kill(&self, target: &str, message: &str) -> Result<()> where Self: Sized {
fn send_kill(&self, target: &str, message: &str) -> Result<()>
where
Self: Sized,
{
self.send(KILL(target.to_owned(), message.to_owned()))
}
/// Kicks the listed nicknames from the listed channels with a comment.
/// If `message` is an empty string, it won't be included in the message.
fn send_kick(&self, chanlist: &str, nicklist: &str, message: &str) -> Result<()>
where Self: Sized {
self.send(KICK(chanlist.to_owned(), nicklist.to_owned(), if message.is_empty() {
None
} else {
Some(message.to_owned())
}))
where
Self: Sized,
{
self.send(KICK(
chanlist.to_owned(),
nicklist.to_owned(),
if message.is_empty() {
None
} else {
Some(message.to_owned())
},
))
}
/// Changes the mode of the target.
/// If `modeparmas` is an empty string, it won't be included in the message.
fn send_mode(&self, target: &str, mode: &str, modeparams: &str) -> Result<()>
where Self: Sized {
self.send(MODE(target.to_owned(), mode.to_owned(), if modeparams.is_empty() {
None
} else {
Some(modeparams.to_owned())
}))
where
Self: Sized,
{
self.send(MODE(
target.to_owned(),
mode.to_owned(),
if modeparams.is_empty() {
None
} else {
Some(modeparams.to_owned())
},
))
}
/// Changes the mode of the target by force.
/// If `modeparams` is an empty string, it won't be included in the message.
fn send_samode(&self, target: &str, mode: &str, modeparams: &str) -> Result<()>
where Self: Sized {
self.send(SAMODE(target.to_owned(), mode.to_owned(), if modeparams.is_empty() {
None
} else {
Some(modeparams.to_owned())
}))
where
Self: Sized,
{
self.send(SAMODE(
target.to_owned(),
mode.to_owned(),
if modeparams.is_empty() {
None
} else {
Some(modeparams.to_owned())
},
))
}
/// Forces a user to change from the old nickname to the new nickname.
fn send_sanick(&self, old_nick: &str, new_nick: &str) -> Result<()> where Self: Sized {
fn send_sanick(&self, old_nick: &str, new_nick: &str) -> Result<()>
where
Self: Sized,
{
self.send(SANICK(old_nick.to_owned(), new_nick.to_owned()))
}
/// Invites a user to the specified channel.
fn send_invite(&self, nick: &str, chan: &str) -> Result<()> where Self: Sized {
fn send_invite(&self, nick: &str, chan: &str) -> Result<()>
where
Self: Sized,
{
self.send(INVITE(nick.to_owned(), chan.to_owned()))
}
/// Quits the server entirely with a message.
/// This defaults to `Powered by Rust.` if none is specified.
fn send_quit(&self, msg: &str) -> Result<()> where Self: Sized {
fn send_quit(&self, msg: &str) -> Result<()>
where
Self: Sized,
{
self.send(QUIT(Some(if msg.is_empty() {
"Powered by Rust.".to_owned()
} else {
@ -173,62 +266,90 @@ pub trait ServerExt: Server {
/// Sends a CTCP-escaped message to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_ctcp(&self, target: &str, msg: &str) -> Result<()> where Self: Sized {
fn send_ctcp(&self, target: &str, msg: &str) -> Result<()>
where
Self: Sized,
{
self.send_privmsg(target, &format!("\u{001}{}\u{001}", msg)[..])
}
/// Sends an action command to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_action(&self, target: &str, msg: &str) -> Result<()> where Self: Sized {
fn send_action(&self, target: &str, msg: &str) -> Result<()>
where
Self: Sized,
{
self.send_ctcp(target, &format!("ACTION {}", msg)[..])
}
/// Sends a finger request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_finger(&self, target: &str) -> Result<()> where Self: Sized {
fn send_finger(&self, target: &str) -> Result<()>
where
Self: Sized,
{
self.send_ctcp(target, "FINGER")
}
/// Sends a version request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_version(&self, target: &str) -> Result<()> where Self: Sized {
fn send_version(&self, target: &str) -> Result<()>
where
Self: Sized,
{
self.send_ctcp(target, "VERSION")
}
/// Sends a source request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_source(&self, target: &str) -> Result<()> where Self: Sized {
fn send_source(&self, target: &str) -> Result<()>
where
Self: Sized,
{
self.send_ctcp(target, "SOURCE")
}
/// Sends a user info request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_user_info(&self, target: &str) -> Result<()> where Self: Sized {
fn send_user_info(&self, target: &str) -> Result<()>
where
Self: Sized,
{
self.send_ctcp(target, "USERINFO")
}
/// Sends a finger request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_ctcp_ping(&self, target: &str) -> Result<()> where Self: Sized {
let time = get_time();
fn send_ctcp_ping(&self, target: &str) -> Result<()>
where
Self: Sized,
{
let time = time::get_time();
self.send_ctcp(target, &format!("PING {}.{}", time.sec, time.nsec)[..])
}
/// Sends a time request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_time(&self, target: &str) -> Result<()> where Self: Sized {
fn send_time(&self, target: &str) -> Result<()>
where
Self: Sized,
{
self.send_ctcp(target, "TIME")
}
}
impl<S> ServerExt for S where S: Server {}
impl<S> ServerExt for S
where
S: Server,
{
}
#[cfg(test)]
mod test {
@ -243,20 +364,29 @@ mod test {
fn identify() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.identify().unwrap();
assert_eq!(&get_server_value(server)[..], "CAP END\r\nNICK :test\r\n\
USER test 0 * :test\r\n");
assert_eq!(
&get_server_value(server)[..],
"CAP END\r\nNICK :test\r\n\
USER test 0 * :test\r\n"
);
}
#[test]
fn identify_with_password() {
let server = IrcServer::from_connection(Config {
nickname: Some(format!("test")),
password: Some(format!("password")),
.. Default::default()
}, MockConnection::empty());
let server = IrcServer::from_connection(
Config {
nickname: Some(format!("test")),
password: Some(format!("password")),
..Default::default()
},
MockConnection::empty(),
);
server.identify().unwrap();
assert_eq!(&get_server_value(server)[..], "CAP END\r\nPASS :password\r\nNICK :test\r\n\
USER test 0 * :test\r\n");
assert_eq!(
&get_server_value(server)[..],
"CAP END\r\nPASS :password\r\nNICK :test\r\n\
USER test 0 * :test\r\n"
);
}
#[test]
@ -270,7 +400,10 @@ mod test {
fn send_join() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_join("#test,#test2,#test3").unwrap();
assert_eq!(&get_server_value(server)[..], "JOIN #test,#test2,#test3\r\n");
assert_eq!(
&get_server_value(server)[..],
"JOIN #test,#test2,#test3\r\n"
);
}
#[test]
@ -291,14 +424,20 @@ mod test {
fn send_privmsg() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_privmsg("#test", "Hi, everybody!").unwrap();
assert_eq!(&get_server_value(server)[..], "PRIVMSG #test :Hi, everybody!\r\n");
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG #test :Hi, everybody!\r\n"
);
}
#[test]
fn send_notice() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_notice("#test", "Hi, everybody!").unwrap();
assert_eq!(&get_server_value(server)[..], "NOTICE #test :Hi, everybody!\r\n");
assert_eq!(
&get_server_value(server)[..],
"NOTICE #test :Hi, everybody!\r\n"
);
}
#[test]
@ -312,14 +451,20 @@ mod test {
fn send_topic() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_topic("#test", "Testing stuff.").unwrap();
assert_eq!(&get_server_value(server)[..], "TOPIC #test :Testing stuff.\r\n");
assert_eq!(
&get_server_value(server)[..],
"TOPIC #test :Testing stuff.\r\n"
);
}
#[test]
fn send_kill() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_kill("test", "Testing kills.").unwrap();
assert_eq!(&get_server_value(server)[..], "KILL test :Testing kills.\r\n");
assert_eq!(
&get_server_value(server)[..],
"KILL test :Testing kills.\r\n"
);
}
#[test]
@ -333,7 +478,10 @@ mod test {
fn send_kick() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_kick("#test", "test", "Testing kicks.").unwrap();
assert_eq!(&get_server_value(server)[..], "KICK #test test :Testing kicks.\r\n");
assert_eq!(
&get_server_value(server)[..],
"KICK #test test :Testing kicks.\r\n"
);
}
#[test]
@ -383,7 +531,10 @@ mod test {
fn send_ctcp() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_ctcp("test", "MESSAGE").unwrap();
assert_eq!(&get_server_value(server)[..], "PRIVMSG test :\u{001}MESSAGE\u{001}\r\n");
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG test :\u{001}MESSAGE\u{001}\r\n"
);
}
#[test]
@ -391,7 +542,10 @@ mod test {
fn send_action() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_action("test", "tests.").unwrap();
assert_eq!(&get_server_value(server)[..], "PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n");
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n"
);
}
#[test]
@ -399,7 +553,10 @@ mod test {
fn send_finger() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_finger("test").unwrap();
assert_eq!(&get_server_value(server)[..], "PRIVMSG test :\u{001}FINGER\u{001}\r\n");
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG test :\u{001}FINGER\u{001}\r\n"
);
}
#[test]
@ -407,7 +564,10 @@ mod test {
fn send_version() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_version("test").unwrap();
assert_eq!(&get_server_value(server)[..], "PRIVMSG test :\u{001}VERSION\u{001}\r\n");
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG test :\u{001}VERSION\u{001}\r\n"
);
}
#[test]
@ -415,7 +575,10 @@ mod test {
fn send_source() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_source("test").unwrap();
assert_eq!(&get_server_value(server)[..], "PRIVMSG test :\u{001}SOURCE\u{001}\r\n");
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG test :\u{001}SOURCE\u{001}\r\n"
);
}
#[test]
@ -423,7 +586,10 @@ mod test {
fn send_user_info() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_user_info("test").unwrap();
assert_eq!(&get_server_value(server)[..], "PRIVMSG test :\u{001}USERINFO\u{001}\r\n");
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG test :\u{001}USERINFO\u{001}\r\n"
);
}
#[test]
@ -442,6 +608,9 @@ mod test {
fn send_time() {
let server = IrcServer::from_connection(test_config(), MockConnection::empty());
server.send_time("test").unwrap();
assert_eq!(&get_server_value(server)[..], "PRIVMSG test :\u{001}TIME\u{001}\r\n");
assert_eq!(
&get_server_value(server)[..],
"PRIVMSG test :\u{001}TIME\u{001}\r\n"
);
}
}

View file

@ -2,11 +2,15 @@
#![warn(missing_docs)]
extern crate bufstream;
extern crate bytes;
extern crate encoding;
extern crate futures;
extern crate native_tls;
extern crate rustc_serialize;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate time;
extern crate tokio_io;
extern crate tokio_core;

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
//! Messages to and from the server.
use std::borrow::ToOwned;
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::io::{Result as IoResult};
use std::io::Result as IoResult;
use std::str::FromStr;
use client::data::Command;
@ -18,14 +18,23 @@ pub struct Message {
impl Message {
/// Creates a new Message.
pub fn new(prefix: Option<&str>, command: &str, args: Vec<&str>, suffix: Option<&str>)
-> IoResult<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: Vec<&str>, suffix: Option<&str>) -> IoResult<Message> {
pub fn with_tags(
tags: Option<Vec<Tag>>,
prefix: Option<&str>,
command: &str,
args: Vec<&str>,
suffix: Option<&str>,
) -> IoResult<Message> {
Ok(Message {
tags: tags,
prefix: prefix.map(|s| s.to_owned()),
@ -37,14 +46,16 @@ impl Message {
pub fn source_nickname(&self) -> Option<&str> {
// <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
// <servername> ::= <host>
self.prefix.as_ref().and_then(|s|
match (s.find('!'), s.find('@'), s.find('.')) {
(Some(i), _, _) => Some(&s[..i]), // <nick> '!' <user> [ '@' <host> ]
(None, Some(i), _) => Some(&s[..i]), // <nick> '@' <host>
(None, None, None) => Some(&s), // <nick>
_ => None // <servername>
}
)
self.prefix.as_ref().and_then(|s| match (
s.find('!'),
s.find('@'),
s.find('.'),
) {
(Some(i), _, _) => Some(&s[..i]), // <nick> '!' <user> [ '@' <host> ]
(None, Some(i), _) => Some(&s[..i]), // <nick> '@' <host>
(None, None, None) => Some(s), // <nick>
_ => None, // <servername>
})
}
/// Converts a Message into a String according to the IRC protocol.
@ -53,7 +64,7 @@ impl Message {
let mut ret = String::new();
if let Some(ref prefix) = self.prefix {
ret.push(':');
ret.push_str(&prefix);
ret.push_str(prefix);
ret.push(' ');
}
let cmd: String = From::from(&self.command);
@ -65,7 +76,11 @@ impl Message {
impl From<Command> for Message {
fn from(cmd: Command) -> Message {
Message { tags: None, prefix: None, command: cmd }
Message {
tags: None,
prefix: None,
command: cmd,
}
}
}
@ -73,44 +88,52 @@ impl FromStr for Message {
type Err = &'static str;
fn from_str(s: &str) -> Result<Message, &'static str> {
let mut state = s;
if s.is_empty() { return Err("Cannot parse an empty string as a message.") }
if s.is_empty() {
return Err("Cannot parse an empty string as a message.");
}
let tags = if state.starts_with('@') {
let tags = state.find(' ').map(|i| &state[1..i]);
state = state.find(' ').map_or("", |i| &state[i+1..]);
tags.map(|ts| ts.split(';').filter(|s| !s.is_empty()).map(|s: &str| {
let mut iter = s.splitn(2, '=');
let (fst, snd) = (iter.next(), iter.next());
Tag(fst.unwrap_or("").to_owned(), snd.map(|s| s.to_owned()))
}).collect::<Vec<_>>())
state = state.find(' ').map_or("", |i| &state[i + 1..]);
tags.map(|ts| {
ts.split(';')
.filter(|s| !s.is_empty())
.map(|s: &str| {
let mut iter = s.splitn(2, '=');
let (fst, snd) = (iter.next(), iter.next());
Tag(fst.unwrap_or("").to_owned(), snd.map(|s| s.to_owned()))
})
.collect::<Vec<_>>()
})
} else {
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..]);
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+2..state.len()-2]);
state = state.find(" :").map_or("", |i| &state[..i+1]);
let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - 2]);
state = state.find(" :").map_or("", |i| &state[..i + 1]);
suffix
} else {
None
};
let command = match state.find(' ').map(|i| &state[..i]) {
Some(cmd) => {
state = state.find(' ').map_or("", |i| &state[i+1..]);
state = state.find(' ').map_or("", |i| &state[i + 1..]);
cmd
}
_ => return Err("Cannot parse a message without a command.")
_ => return Err("Cannot parse a message without a command."),
};
if suffix.is_none() { state = &state[..state.len() - 2] }
if suffix.is_none() {
state = &state[..state.len() - 2]
}
let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect();
Message::with_tags(
tags, prefix, command, args, suffix
).map_err(|_| "Invalid input for Command.")
Message::with_tags(tags, prefix, command, args, suffix)
.map_err(|_| "Invalid input for Command.")
}
}
@ -142,42 +165,69 @@ mod test {
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!(Message::new(None, "PRIVMSG", vec!["test"], Some("Testing!")).unwrap(), message)
assert_eq!(
Message::new(None, "PRIVMSG", vec!["test"], Some("Testing!")).unwrap(),
message
)
}
#[test]
fn source_nickname() {
assert_eq!(Message::new(
None, "PING", vec![], Some("data")
).unwrap().source_nickname(), None);
assert_eq!(
Message::new(None, "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
None
);
assert_eq!(Message::new(
Some("irc.test.net"), "PING", vec![], Some("data")
).unwrap().source_nickname(), None);
assert_eq!(
Message::new(Some("irc.test.net"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
None
);
assert_eq!(Message::new(
Some("test!test@test"), "PING", vec![], Some("data")
).unwrap().source_nickname(), Some("test"));
assert_eq!(
Message::new(Some("test!test@test"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(Message::new(
Some("test@test"), "PING", vec![], Some("data")
).unwrap().source_nickname(), Some("test"));
assert_eq!(
Message::new(Some("test@test"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(Message::new(
Some("test!test@irc.test.com"), "PING", vec![], Some("data")
).unwrap().source_nickname(), Some("test"));
assert_eq!(
Message::new(Some("test!test@irc.test.com"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(Message::new(
Some("test!test@127.0.0.1"), "PING", vec![], Some("data")
).unwrap().source_nickname(), Some("test"));
assert_eq!(
Message::new(Some("test!test@127.0.0.1"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(Message::new(
Some("test@test.com"), "PING", vec![], Some("data")
).unwrap().source_nickname(), Some("test"));
assert_eq!(
Message::new(Some("test@test.com"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(Message::new(
Some("test"), "PING", vec![], Some("data")
).unwrap().source_nickname(), Some("test"));
assert_eq!(
Message::new(Some("test"), "PING", vec![], Some("data"))
.unwrap()
.source_nickname(),
Some("test")
);
}
#[test]
@ -193,7 +243,10 @@ mod test {
prefix: Some(format!("test!test@test")),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
assert_eq!(&message.to_string()[..], ":test!test@test PRIVMSG test :Still testing!\r\n");
assert_eq!(
&message.to_string()[..],
":test!test@test PRIVMSG test :Still testing!\r\n"
);
}
#[test]
@ -209,16 +262,25 @@ mod test {
prefix: Some(format!("test!test@test")),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
assert_eq!(":test!test@test PRIVMSG test :Still testing!\r\n".parse(), Ok(message));
assert_eq!(
":test!test@test PRIVMSG test :Still testing!\r\n".parse(),
Ok(message)
);
let message = Message {
tags: Some(vec![Tag(format!("aaa"), Some(format!("bbb"))),
Tag(format!("ccc"), None),
Tag(format!("example.com/ddd"), Some(format!("eee")))]),
tags: Some(vec![
Tag(format!("aaa"), Some(format!("bbb"))),
Tag(format!("ccc"), None),
Tag(format!("example.com/ddd"), Some(format!("eee"))),
]),
prefix: Some(format!("test!test@test")),
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))
assert_eq!(
"@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
tags!\r\n"
.parse(),
Ok(message)
)
}
#[test]
@ -247,7 +309,9 @@ mod test {
tags: None,
prefix: Some(format!("test!test@test")),
command: Raw(
format!("COMMAND"), vec![format!("ARG:test")], Some(format!("Testing!"))
format!("COMMAND"),
vec![format!("ARG:test")],
Some(format!("Testing!")),
),
};
let msg: Message = ":test!test@test COMMAND ARG:test :Testing!\r\n".into();

View file

@ -28,15 +28,15 @@ macro_rules! make_response {
make_response! {
// Expected replies
/// `001 Welcome to the Internet Relay Network <nick>!<user>@<host>`
RPL_WELCOME = 001,
RPL_WELCOME = 1,
/// `002 Your host is <servername>, running version <ver>`
RPL_YOURHOST = 002,
RPL_YOURHOST = 2,
/// `003 This server was created <date>`
RPL_CREATED = 003,
RPL_CREATED = 3,
/// `004 <servername> <version> <available user modes> available channel modes>`
RPL_MYINFO = 004,
RPL_MYINFO = 4,
/// `005 Try server <server name>, port <port number>`
RPL_BOUNCE = 005,
RPL_BOUNCE = 5,
/// `302 :*1<reply> *( " " <reply> )`
RPL_USERHOST = 302,
/// `303 :*1<nick> *( " " <nick> )`
@ -77,6 +77,8 @@ make_response! {
RPL_NOTOPIC = 331,
/// `332 <channel> :<topic>`
RPL_TOPIC = 332,
/// `333 <channel> <nick>!<user>@<host> <unix timestamp>`
RPL_TOPICWHOTIME = 333,
/// `341 <channel> <nick>`
RPL_INVITING = 341,
/// `342 <user> :Summoning user to IRC`
@ -367,7 +369,7 @@ impl FromStr for Response {
if let Ok(rc) = s.parse() {
match Response::from_u16(rc) {
Some(r) => Ok(r),
None => Err("Failed to parse due to unknown response code.")
None => Err("Failed to parse due to unknown response code."),
}
} else {
Err("Failed to parse response code.")

View file

@ -1,3 +1,3 @@
//! A simple, thread-safe IRC server library.
//! The server module is currently unimplimented. Visit
//! The server module is currently unimplimented. Visit
//! https://github.com/aatxe/irc/issues/22 to contribute!