#[cfg(feature = "ctcp")]
use std::ascii::AsciiExt;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
#[cfg(feature = "ctcp")]
use chrono::prelude::*;
use futures::{Async, Poll, Future, Sink, Stream};
use futures::stream::SplitStream;
use futures::sync::mpsc;
use futures::sync::oneshot;
use futures::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use tokio_core::reactor::{Core, Handle};
use error;
use client::conn::{Connection, ConnectionFuture};
use client::data::{Config, User};
use client::ext::ClientExt;
use client::transport::LogView;
use proto::{ChannelMode, Command, Message, Mode, Response};
use proto::Command::{JOIN, KICK, NICK, NICKSERV, PART, PRIVMSG, ChannelMODE, QUIT};
pub mod conn;
pub mod data;
pub mod ext;
pub mod prelude;
pub mod reactor;
pub mod transport;
pub trait EachIncomingExt: Stream<Item=Message, Error=error::IrcError> {
fn for_each_incoming<F>(self, mut f: F) -> error::Result<()>
where F: FnMut(Message) -> (), Self: Sized {
self.for_each(|msg| {
f(msg);
Ok(())
}).wait()
}
}
impl<T> EachIncomingExt for T where T: Stream<Item=Message, Error=error::IrcError> {}
pub trait Client {
fn config(&self) -> &Config;
fn send<M: Into<Message>>(&self, message: M) -> error::Result<()> where Self: Sized;
fn stream(&self) -> ClientStream;
fn for_each_incoming<F>(&self, f: F) -> error::Result<()> where F: FnMut(Message) -> () {
self.stream().for_each_incoming(f)
}
fn list_channels(&self) -> Option<Vec<String>>;
fn list_users(&self, channel: &str) -> Option<Vec<User>>;
}
#[derive(Debug)]
pub struct ClientStream {
state: Arc<ClientState>,
stream: SplitStream<Connection>,
}
impl Stream for ClientStream {
type Item = Message;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
match try_ready!(self.stream.poll()) {
Some(msg) => {
self.state.handle_message(&msg)?;
Ok(Async::Ready(Some(msg)))
}
None => Ok(Async::Ready(None)),
}
}
}
#[derive(Debug)]
struct ClientState {
config: Config,
chanlists: Mutex<HashMap<String, Vec<User>>>,
alt_nick_index: RwLock<usize>,
incoming: Mutex<Option<SplitStream<Connection>>>,
outgoing: UnboundedSender<Message>,
}
impl<'a> Client for ClientState {
fn config(&self) -> &Config {
&self.config
}
fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()> where Self: Sized {
let msg = msg.into();
self.handle_sent_message(&msg)?;
Ok(self.outgoing.unbounded_send(msg)?)
}
fn stream(&self) -> ClientStream {
unimplemented!()
}
#[cfg(not(feature = "nochanlists"))]
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>> {
None
}
}
impl ClientState {
fn new(
incoming: SplitStream<Connection>,
outgoing: UnboundedSender<Message>,
config: Config,
) -> ClientState {
ClientState {
config: config,
chanlists: Mutex::new(HashMap::new()),
alt_nick_index: RwLock::new(0),
incoming: Mutex::new(Some(incoming)),
outgoing: outgoing,
}
}
fn current_nickname(&self) -> &str {
let alt_nicks = self.config().alternate_nicknames();
let index = self.alt_nick_index.read().unwrap();
match *index {
0 => self.config().nickname().expect(
"current_nickname should not be callable if nickname is not defined."
),
i => alt_nicks[i - 1],
}
}
fn handle_sent_message(&self, msg: &Message) -> error::Result<()> {
trace!("[SENT] {}", msg.to_string());
match msg.command {
PART(ref chan, _) => {
let _ = self.chanlists.lock().unwrap().remove(chan);
}
_ => (),
}
Ok(())
}
fn handle_message(&self, msg: &Message) -> error::Result<()> {
trace!("[RECV] {}", msg.to_string());
match msg.command {
JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan),
PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan),
KICK(ref chan, ref user, _) => self.handle_part(user, 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)
}
ChannelMODE(ref chan, ref modes) => self.handle_mode(chan, modes),
PRIVMSG(ref target, ref body) => {
if body.starts_with('\u{001}') {
let tokens: Vec<_> = {
let end = if body.ends_with('\u{001}') {
body.len() - 1
} else {
body.len()
};
body[1..end].split(' ').collect()
};
if target.starts_with('#') {
self.handle_ctcp(target, &tokens)?
} else if let Some(user) = msg.source_nickname() {
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, _, _) => {
self.send_nick_password()?;
self.send_umodes()?;
let config_chans = self.config().channels();
for chan in &config_chans {
match self.config().channel_key(chan) {
Some(key) => self.send_join_with_keys(chan, key)?,
None => self.send_join(chan)?,
}
}
let joined_chans = self.chanlists.lock().unwrap();
for chan in joined_chans.keys().filter(
|x| !config_chans.contains(&x.as_str()),
)
{
self.send_join(chan)?
}
}
Command::Response(Response::ERR_NICKNAMEINUSE, _, _) |
Command::Response(Response::ERR_ERRONEOUSNICKNAME, _, _) => {
let alt_nicks = self.config().alternate_nicknames();
let mut index = self.alt_nick_index.write().unwrap();
if *index >= alt_nicks.len() {
return Err(error::IrcError::NoUsableNick);
} else {
self.send(NICK(alt_nicks[*index].to_owned()))?;
*index += 1;
}
}
_ => (),
}
Ok(())
}
fn send_nick_password(&self) -> error::Result<()> {
if self.config().nick_password().is_empty() {
Ok(())
} else {
let mut index = self.alt_nick_index.write().unwrap();
if self.config().should_ghost() && *index != 0 {
for seq in &self.config().ghost_sequence() {
self.send(NICKSERV(format!(
"{} {} {}",
seq,
self.config().nickname()?,
self.config().nick_password()
)))?;
}
*index = 0;
self.send(NICK(self.config().nickname()?.to_owned()))?
}
self.send(NICKSERV(
format!("IDENTIFY {}", self.config().nick_password()),
))
}
}
fn send_umodes(&self) -> error::Result<()> {
if self.config().umodes().is_empty() {
Ok(())
} else {
self.send_mode(
self.current_nickname(), &Mode::as_user_modes(self.config().umodes()).map_err(|e| {
error::IrcError::InvalidMessage {
string: format!(
"MODE {} {}", self.current_nickname(), self.config().umodes()
),
cause: e,
}
})?
)
}
}
#[cfg(feature = "nochanlists")]
fn handle_join(&self, _: &str, _: &str) {}
#[cfg(not(feature = "nochanlists"))]
fn handle_join(&self, src: &str, chan: &str) {
if let Some(vec) = self.chanlists.lock().unwrap().get_mut(&chan.to_owned()) {
if !src.is_empty() {
vec.push(User::new(src))
}
}
}
#[cfg(feature = "nochanlists")]
fn handle_part(&self, src: &str, chan: &str) {}
#[cfg(not(feature = "nochanlists"))]
fn handle_part(&self, src: &str, chan: &str) {
if let Some(vec) = self.chanlists.lock().unwrap().get_mut(&chan.to_owned()) {
if !src.is_empty() {
if let Some(n) = vec.iter().position(|x| x.get_nickname() == src) {
vec.swap_remove(n);
}
}
}
}
#[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, _: &str, _: &[Mode<ChannelMODE>]) {}
#[cfg(not(feature = "nochanlists"))]
fn handle_mode(&self, chan: &str, modes: &[Mode<ChannelMode>]) {
for mode in modes {
match *mode {
Mode::Plus(_, Some(ref user)) | Mode::Minus(_, Some(ref user)) => {
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)
}
}
}
_ => (),
}
}
}
#[cfg(feature = "nochanlists")]
fn handle_namreply(&self, _: &[String], _: &Option<String>) {}
#[cfg(not(feature = "nochanlists"))]
fn handle_namreply(&self, args: &[String], suffix: &Option<String>) {
if let Some(ref users) = *suffix {
if args.len() == 3 {
let chan = &args[2];
for user in users.split(' ') {
let mut chanlists = self.chanlists.lock().unwrap();
chanlists
.entry(chan.clone())
.or_insert_with(Vec::new)
.push(User::new(user))
}
}
}
}
#[cfg(feature = "ctcp")]
fn handle_ctcp(&self, resp: &str, tokens: &[&str]) -> error::Result<()> {
if tokens.is_empty() {
return Ok(());
}
if tokens[0].eq_ignore_ascii_case("FINGER") {
self.send_ctcp_internal(
resp,
&format!(
"FINGER :{} ({})",
self.config().real_name(),
self.config().username()
),
)
} else if tokens[0].eq_ignore_ascii_case("VERSION") {
self.send_ctcp_internal(resp, &format!("VERSION {}", self.config().version()))
} else if tokens[0].eq_ignore_ascii_case("SOURCE") {
self.send_ctcp_internal(
resp,
&format!("SOURCE {}", self.config().source()),
)?;
self.send_ctcp_internal(resp, "SOURCE")
} else if tokens[0].eq_ignore_ascii_case("PING") && tokens.len() > 1 {
self.send_ctcp_internal(resp, &format!("PING {}", tokens[1]))
} else if tokens[0].eq_ignore_ascii_case("TIME") {
self.send_ctcp_internal(resp, &format!("TIME :{}", Local::now().to_rfc2822()))
} else if tokens[0].eq_ignore_ascii_case("USERINFO") {
self.send_ctcp_internal(resp, &format!("USERINFO :{}", self.config().user_info()))
} else {
Ok(())
}
}
#[cfg(feature = "ctcp")]
fn send_ctcp_internal(&self, target: &str, msg: &str) -> error::Result<()> {
self.send_notice(target, &format!("\u{001}{}\u{001}", msg))
}
#[cfg(not(feature = "ctcp"))]
fn handle_ctcp(&self, _: &str, _: &[&str]) -> error::Result<()> {
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct IrcClient {
state: Arc<ClientState>,
view: Option<LogView>,
}
impl Client for IrcClient {
fn config(&self) -> &Config {
&self.state.config
}
fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()>
where
Self: Sized,
{
self.state.send(msg)
}
fn stream(&self) -> ClientStream {
ClientStream {
state: Arc::clone(&self.state),
stream: self.state.incoming.lock().unwrap().take().expect(
"Stream was already obtained once, and cannot be reobtained."
),
}
}
#[cfg(not(feature = "nochanlists"))]
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>> {
None
}
}
impl IrcClient {
pub fn new<P: AsRef<Path>>(config: P) -> error::Result<IrcClient> {
IrcClient::from_config(Config::load(config)?)
}
pub fn from_config(config: Config) -> error::Result<IrcClient> {
let (tx_outgoing, rx_outgoing) = mpsc::unbounded();
let (tx_incoming, rx_incoming) = oneshot::channel();
let (tx_view, rx_view) = oneshot::channel();
let cfg = config.clone();
let _ = thread::spawn(move || {
let mut reactor = Core::new().unwrap();
let handle = reactor.handle();
let conn = reactor.run(Connection::new(&cfg, &handle).unwrap()).unwrap();
tx_view.send(conn.log_view()).unwrap();
let (sink, stream) = conn.split();
let outgoing_future = sink.send_all(rx_outgoing.map_err::<error::IrcError, _>(|_| {
unreachable!("futures::sync::mpsc::Receiver should never return Err");
})).map(|_| ()).map_err(|e| panic!("{}", e));
tx_incoming.send(stream).unwrap();
reactor.run(outgoing_future).unwrap();
});
Ok(IrcClient {
state: Arc::new(ClientState::new(rx_incoming.wait()?, tx_outgoing, config)),
view: rx_view.wait()?,
})
}
pub fn new_future(handle: Handle, config: &Config) -> error::Result<IrcClientFuture> {
let (tx_outgoing, rx_outgoing) = mpsc::unbounded();
Ok(IrcClientFuture {
conn: Connection::new(config, &handle)?,
_handle: handle,
config: config,
tx_outgoing: Some(tx_outgoing),
rx_outgoing: Some(rx_outgoing),
})
}
pub fn current_nickname(&self) -> &str {
self.state.current_nickname()
}
#[cfg(test)]
fn log_view(&self) -> &LogView {
self.view.as_ref().unwrap()
}
}
#[derive(Debug)]
pub struct IrcClientFuture<'a> {
conn: ConnectionFuture<'a>,
_handle: Handle,
config: &'a Config,
tx_outgoing: Option<UnboundedSender<Message>>,
rx_outgoing: Option<UnboundedReceiver<Message>>,
}
impl<'a> Future for IrcClientFuture<'a> {
type Item = PackedIrcClient;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let conn = try_ready!(self.conn.poll());
let view = conn.log_view();
let (sink, stream) = conn.split();
let outgoing_future = sink.send_all(
self.rx_outgoing.take().unwrap().map_err::<error::IrcError, _>(|()| {
unreachable!("futures::sync::mpsc::Receiver should never return Err");
})
).map(|_| ());
let server = IrcClient {
state: Arc::new(ClientState::new(
stream, self.tx_outgoing.take().unwrap(), self.config.clone()
)),
view: view,
};
Ok(Async::Ready(PackedIrcClient(server, Box::new(outgoing_future))))
}
}
pub struct PackedIrcClient(pub IrcClient, pub Box<Future<Item = (), Error = error::IrcError>>);
#[cfg(test)]
mod test {
use std::collections::HashMap;
use std::default::Default;
use std::thread;
use std::time::Duration;
use super::{IrcClient, Client};
use error::IrcError;
use client::data::Config;
#[cfg(not(feature = "nochanlists"))]
use client::data::User;
use proto::{ChannelMode, IrcCodec, Mode};
use proto::command::Command::{PART, PRIVMSG, Raw};
pub fn test_config() -> Config {
Config {
owners: Some(vec![format!("test")]),
nickname: Some(format!("test")),
alt_nicks: Some(vec![format!("test2")]),
server: Some(format!("irc.test.net")),
channels: Some(vec![format!("#test"), format!("#test2")]),
user_info: Some(format!("Testing.")),
use_mock_connection: Some(true),
..Default::default()
}
}
pub fn get_client_value(client: IrcClient) -> String {
thread::sleep(Duration::from_millis(100));
client.log_view().sent().unwrap().iter().fold(String::new(), |mut acc, msg| {
acc.push_str(&IrcCodec::sanitize(msg.to_string()));
acc
})
}
#[test]
fn stream() {
let exp = "PRIVMSG test :Hi!\r\nPRIVMSG test :This is a test!\r\n\
:test!test@test JOIN #test\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(exp.to_owned()),
..test_config()
}).unwrap();
let mut messages = String::new();
client.for_each_incoming(|message| {
messages.push_str(&message.to_string());
}).unwrap();
assert_eq!(&messages[..], exp);
}
#[test]
fn handle_message() {
let value = ":irc.test.net 376 test :End of /MOTD command.\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"JOIN #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 client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
nick_password: Some(format!("password")),
channels: Some(vec![format!("#test"), format!("#test2")]),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"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 client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
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)
},
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"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 client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
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),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"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 client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
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")]),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"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"
);
}
#[test]
fn handle_end_motd_with_umodes() {
let value = ":irc.test.net 376 test :End of /MOTD command.\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
nickname: Some(format!("test")),
umodes: Some(format!("+B")),
channels: Some(vec![format!("#test"), format!("#test2")]),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"MODE test +B\r\nJOIN #test\r\nJOIN #test2\r\n"
);
}
#[test]
fn nickname_in_use() {
let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(&get_client_value(client)[..], "NICK :test2\r\n");
}
#[test]
fn ran_out_of_nicknames() {
let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\
:irc.pdgn.co 433 * test2 :Nickname is already in use.\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
let res = client.for_each_incoming(|message| {
println!("{:?}", message);
});
if let Err(IrcError::NoUsableNick) = res {
()
} else {
panic!("expected error when no valid nicks were specified")
}
}
#[test]
fn send() {
let client = IrcClient::from_config(test_config()).unwrap();
assert!(
client
.send(PRIVMSG(format!("#test"), format!("Hi there!")))
.is_ok()
);
assert_eq!(
&get_client_value(client)[..],
"PRIVMSG #test :Hi there!\r\n"
);
}
#[test]
fn send_no_newline_injection() {
let client = IrcClient::from_config(test_config()).unwrap();
assert!(
client
.send(PRIVMSG(format!("#test"), format!("Hi there!\r\nJOIN #bad")))
.is_ok()
);
assert_eq!(&get_client_value(client)[..], "PRIVMSG #test :Hi there!\r\n");
}
#[test]
fn send_raw_is_really_raw() {
let client = IrcClient::from_config(test_config()).unwrap();
assert!(
client.send(Raw("PASS".to_owned(), vec!["password".to_owned()], None)).is_ok()
);
assert!(
client.send(Raw("NICK".to_owned(), vec!["test".to_owned()], None)).is_ok()
);
assert_eq!(&get_client_value(client)[..], "PASS password\r\nNICK test\r\n");
}
#[test]
#[cfg(not(feature = "nochanlists"))]
fn channel_tracking_names() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(client.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 client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert!(client.send(PART(format!("#test"), None)).is_ok());
assert!(client.list_channels().unwrap().is_empty())
}
#[test]
#[cfg(not(feature = "nochanlists"))]
fn user_tracking_names() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
client.list_users("#test").unwrap(),
vec![User::new("test"), User::new("~owner"), User::new("&admin")]
)
}
#[test]
#[cfg(not(feature = "nochanlists"))]
fn user_tracking_names_join() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\
:test2!test@test JOIN #test\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
client.list_users("#test").unwrap(),
vec![
User::new("test"),
User::new("~owner"),
User::new("&admin"),
User::new("test2"),
]
)
}
#[test]
#[cfg(not(feature = "nochanlists"))]
fn user_tracking_names_kick() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\
:owner!test@test KICK #test test\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
client.list_users("#test").unwrap(),
vec![
User::new("&admin"),
User::new("~owner"),
]
)
}
#[test]
#[cfg(not(feature = "nochanlists"))]
fn user_tracking_names_part() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\
:owner!test@test PART #test\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
client.list_users("#test").unwrap(),
vec![User::new("test"), User::new("&admin")]
)
}
#[test]
#[cfg(not(feature = "nochanlists"))]
fn user_tracking_names_mode() {
let value = ":irc.test.net 353 test = #test :+test ~owner &admin\r\n\
:test!test@test MODE #test +o test\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
client.list_users("#test").unwrap(),
vec![User::new("@test"), User::new("~owner"), User::new("&admin")]
);
let mut exp = User::new("@test");
exp.update_access_level(&Mode::Plus(ChannelMode::Voice, None));
assert_eq!(
client.list_users("#test").unwrap()[0].highest_access_level(),
exp.highest_access_level()
);
let mut levels = client.list_users("#test").unwrap()[0].access_levels();
levels.retain(|l| exp.access_levels().contains(l));
assert_eq!(levels.len(), exp.access_levels().len());
}
#[test]
#[cfg(feature = "nochanlists")]
fn no_user_tracking() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert!(client.list_users("#test").is_none())
}
#[test]
#[cfg(feature = "ctcp")]
fn finger_response() {
let value = ":test!test@test PRIVMSG test :\u{001}FINGER\u{001}\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"NOTICE test :\u{001}FINGER :test (test)\u{001}\r\n"
);
}
#[test]
#[cfg(feature = "ctcp")]
fn version_response() {
let value = ":test!test@test PRIVMSG test :\u{001}VERSION\u{001}\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
&format!(
"NOTICE test :\u{001}VERSION {}\u{001}\r\n",
::VERSION_STR,
)
);
}
#[test]
#[cfg(feature = "ctcp")]
fn source_response() {
let value = ":test!test@test PRIVMSG test :\u{001}SOURCE\u{001}\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"NOTICE test :\u{001}SOURCE https://github.com/aatxe/irc\u{001}\r\n\
NOTICE test :\u{001}SOURCE\u{001}\r\n"
);
}
#[test]
#[cfg(feature = "ctcp")]
fn ctcp_ping_response() {
let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"NOTICE test :\u{001}PING test\u{001}\r\n"
);
}
#[test]
#[cfg(feature = "ctcp")]
fn time_response() {
let value = ":test!test@test PRIVMSG test :\u{001}TIME\u{001}\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
let val = get_client_value(client);
assert!(val.starts_with("NOTICE test :\u{001}TIME :"));
assert!(val.ends_with("\u{001}\r\n"));
}
#[test]
#[cfg(feature = "ctcp")]
fn user_info_response() {
let value = ":test!test@test PRIVMSG test :\u{001}USERINFO\u{001}\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_client_value(client)[..],
"NOTICE test :\u{001}USERINFO :Testing.\u{001}\
\r\n"
);
}
#[test]
#[cfg(feature = "ctcp")]
fn ctcp_ping_no_timestamp() {
let value = ":test!test@test PRIVMSG test :\u{001}PING\u{001}\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(&get_client_value(client)[..], "");
}
}