rust-irc/src/client/server/mod.rs
2017-06-28 22:24:23 -07:00

1059 lines
36 KiB
Rust

//! Interface for working with IRC Servers.
#[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::UnboundedSender;
use tokio_core::reactor::Core;
use error;
use client::conn::Connection;
use client::data::{Config, User};
use client::server::utils::ServerExt;
use client::transport::LogView;
use proto::{ChannelMode, Command, Message, Mode, Response};
use proto::Command::{JOIN, NICK, NICKSERV, PART, PRIVMSG, ChannelMODE, QUIT};
pub mod utils;
/// Trait extending all IRC streams with `for_each_incoming` convenience function.
pub trait EachIncomingExt: Stream<Item=Message, Error=error::Error> {
/// Blocks on the stream, running the given function on each incoming message as they arrive.
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::Error> {}
/// An interface for interacting with an IRC server.
pub trait Server {
/// Gets the configuration being used with this Server.
fn config(&self) -> &Config;
/// Sends a Command to this Server.
fn send<M: Into<Message>>(&self, message: M) -> error::Result<()>
where
Self: Sized;
/// Gets a stream of incoming messages from the Server.
/// Note: The stream can only be gotten once. Subsequent attempts will panic.
fn stream(&self) -> ServerStream;
/// Blocks on the stream, running the given function on each incoming message as they arrive.
fn for_each_incoming<F>(&self, f: F) -> error::Result<()>
where
F: FnMut(Message) -> (),
{
self.stream().for_each_incoming(f)
}
/// Gets a list of currently joined channels. This will be none if tracking is not supported
/// altogether (such as when the `nochanlists` feature is enabled).
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.
fn list_users(&self, channel: &str) -> Option<Vec<User>>;
}
/// A stream of `Messages` from the `IrcServer`. Interaction with this stream relies on the
/// `futures` API.
pub struct ServerStream {
state: Arc<ServerState>,
stream: SplitStream<Connection>,
}
impl Stream for ServerStream {
type Item = Message;
type Error = error::Error;
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)),
}
}
}
/// Thread-safe internal state for an IRC server connection.
struct ServerState {
/// The configuration used with this connection.
config: Config,
/// A thread-safe map of channels to the list of users in them.
chanlists: Mutex<HashMap<String, Vec<User>>>,
/// A thread-safe index to track the current alternative nickname being used.
alt_nick_index: RwLock<usize>,
/// A thread-safe internal IRC stream used for the reading API.
incoming: Mutex<Option<SplitStream<Connection>>>,
/// A thread-safe copy of the outgoing channel.
outgoing: UnboundedSender<Message>,
}
impl<'a> Server for ServerState {
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).send(
ServerState::sanitize(&msg.to_string())
.into(),
)?)
}
fn stream(&self) -> ServerStream {
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 ServerState {
fn new(
incoming: SplitStream<Connection>,
outgoing: UnboundedSender<Message>,
config: Config,
) -> ServerState {
ServerState {
config: config,
chanlists: Mutex::new(HashMap::new()),
alt_nick_index: RwLock::new(0),
incoming: Mutex::new(Some(incoming)),
outgoing: outgoing,
}
}
/// Sanitizes the input string by cutting up to (and including) the first occurence of a line
/// terminiating phrase (`\r\n`, `\r`, or `\n`). This is used in sending messages back to
/// prevent the injection of additional commands.
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
}
}
/// Gets the current nickname in use.
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(),
i => alt_nicks[i - 1],
}
}
/// Handles sent messages internally for basic client functionality.
fn handle_sent_message(&self, msg: &Message) -> error::Result<()> {
match msg.command {
PART(ref chan, _) => {
let _ = self.chanlists.lock().unwrap().remove(chan);
}
_ => (),
}
Ok(())
}
/// Handles received messages internally for basic client functionality.
fn handle_message(&self, msg: &Message) -> error::Result<()> {
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),
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() {
panic!("All specified nicknames were in use or disallowed.")
} 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())?)
}
}
#[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: Vec<&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, _: Vec<&str>) -> error::Result<()> {
Ok(())
}
}
/// A thread-safe implementation of an IRC Server connection.
#[derive(Clone)]
pub struct IrcServer {
/// The internal, thread-safe server state.
state: Arc<ServerState>,
/// A view of the logs for a mock connection.
view: Option<LogView>,
}
impl Server for IrcServer {
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) -> ServerStream {
ServerStream {
state: self.state.clone(),
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 IrcServer {
/// Creates a new IRC Server connection from the configuration at the specified path,
/// connecting immediately.
pub fn new<P: AsRef<Path>>(config: P) -> error::Result<IrcServer> {
IrcServer::from_config(Config::load(config)?)
}
/// Creates a new IRC server connection from the specified configuration, connecting
/// immediately.
pub fn from_config(config: Config) -> error::Result<IrcServer> {
// Setting up a remote reactor running for the length of the connection.
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();
// Setting up internal processing stuffs.
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(|_| {
let res: error::Error = error::ErrorKind::ChannelError.into();
res
})).map(|_| ()).map_err(|e| panic!("{}", e));
// Send the stream half back to the original thread.
tx_incoming.send(stream).unwrap();
reactor.run(outgoing_future).unwrap();
});
Ok(IrcServer {
state: Arc::new(ServerState::new(rx_incoming.wait()?, tx_outgoing, config)),
view: rx_view.wait()?,
})
}
/// Gets the current nickname in use.
pub fn current_nickname(&self) -> &str {
self.state.current_nickname()
}
/// Gets the log view from the internal transport. Only used for unit testing.
#[cfg(test)]
fn log_view(&self) -> &LogView {
self.view.as_ref().unwrap()
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use std::default::Default;
use std::thread;
use std::time::Duration;
use super::{IrcServer, Server};
use client::data::Config;
#[cfg(not(feature = "nochanlists"))]
use client::data::User;
use proto::{ChannelMode, Mode};
use proto::command::Command::{PART, PRIVMSG};
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_server_value(server: IrcServer) -> String {
// We sleep here because of synchronization issues.
// We can't guarantee that everything will have been sent by the time of this call.
thread::sleep(Duration::from_millis(100));
server.log_view().sent().unwrap().iter().fold(String::new(), |mut acc, msg| {
acc.push_str(&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 server = IrcServer::from_config(Config {
mock_initial_value: Some(exp.to_owned()),
..test_config()
}).unwrap();
let mut messages = String::new();
server.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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_server_value(server)[..],
"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 server = IrcServer::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();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
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_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();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
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_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();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
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_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();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
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"
);
}
#[test]
fn handle_end_motd_with_umodes() {
let value = ":irc.test.net 376 test :End of /MOTD command.\r\n";
let server = IrcServer::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();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_server_value(server)[..],
"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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(&get_server_value(server)[..], "NICK :test2\r\n");
}
#[test]
#[should_panic(expected = "All specified nicknames were in use or disallowed.")]
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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
}
#[test]
fn send() {
let server = IrcServer::from_config(test_config()).unwrap();
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_config(test_config()).unwrap();
assert!(
server
.send(PRIVMSG(format!("#test"), format!("Hi there!\r\nJOIN #bad")))
.is_ok()
);
assert_eq!(&get_server_value(server)[..], "PRIVMSG #test :Hi there!\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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
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_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert!(server.send(PART(format!("#test"), None)).is_ok());
assert!(server.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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
server.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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
server.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_part() {
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\
:owner!test@test PART #test\r\n";
let server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
server.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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
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(&Mode::Plus(ChannelMode::Voice, None));
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();
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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert!(server.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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_server_value(server)[..],
"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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_server_value(server)[..],
&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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
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]
#[cfg(feature = "ctcp")]
fn ctcp_ping_response() {
let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n";
let server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_server_value(server)[..],
"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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
let val = get_server_value(server);
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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(
&get_server_value(server)[..],
"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 server = IrcServer::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
server.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(&get_server_value(server)[..], "");
}
}