Merge branch 'master' into async
This commit is contained in:
commit
cf3ee671ed
17 changed files with 2145 additions and 1252 deletions
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
1937
src/proto/command.rs
1937
src/proto/command.rs
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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!
|
||||
|
|
Loading…
Add table
Reference in a new issue