diff --git a/.gitignore b/.gitignore index c00c28f..dfb0af2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,4 @@ /irc-proto/target /Cargo.lock /irc-proto/Cargo.lock -*.json -*.toml -*.yaml !Cargo.toml diff --git a/.travis.yml b/.travis.yml index cc60af0..62e3949 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,11 @@ language: rust rust: stable sudo: false script: - - chmod +x mktestconfig.sh - - ./mktestconfig.sh - - pushd irc-proto - - cargo test --verbose - - popd - - cargo build --verbose - - rustdoc --test README.md --extern irc=target/debug/libirc.rlib -L target/debug/deps - - cargo test --verbose --features "toml yaml" - - cargo test --doc - - cargo test --verbose --no-default-features - - cargo run --bin build-bot + - cargo test --all --features "toml yaml json" + - cargo build + # No idea how to fix this, since we don't depend on futures-preview directly. + # - rustdoc --test README.md --extern irc=target/debug/libirc.rlib -L target/debug/deps --edition 2018 + - cargo run --example build-bot notifications: email: false irc: diff --git a/Cargo.toml b/Cargo.toml index 3b5e45d..15719d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ categories = ["asynchronous", "network-programming"] documentation = "https://docs.rs/irc/" repository = "https://github.com/aatxe/irc" readme = "README.md" +edition = "2018" [badges] travis-ci = { repository = "aatxe/irc" } @@ -31,22 +32,25 @@ bytes = "0.4" chrono = "0.4" encoding = "0.2" failure = "0.1" -futures = "0.1" irc-proto = { version = "*", path = "irc-proto" } log = "0.4" native-tls = "0.2" -serde = "1.0" +serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" +tokio = { version = "0.2.4", features = ["time", "net", "stream", "macros", "stream"] } +tokio-util = { version = "0.2.0", features = ["codec"] } +tokio-tls = "0.3.0" serde_json = { version = "1.0", optional = true } serde_yaml = { version = "0.7", optional = true } -tokio = "0.1" -tokio-codec = "0.1" -tokio-io = "0.1" -tokio-mockstream = "1.1" -tokio-timer = "0.1" -tokio-tls = "0.2" toml = { version = "0.4", optional = true } +pin-utils = "0.1.0-alpha.4" +parking_lot = "0.9.0" +futures-channel = "0.3.1" +futures-util = "0.3.1" [dev-dependencies] +futures = "0.3.1" +anyhow = "1.0.13" args = "2.0" getopts = "0.2" +env_logger = "0.6.2" \ No newline at end of file diff --git a/README.md b/README.md index 84007cf..90510b2 100644 --- a/README.md +++ b/README.md @@ -48,21 +48,16 @@ documentation, and below. [irc-prelude]: https://docs.rs/irc/*/irc/client/prelude/index.html -## A Tale of Two APIs +## Using Futures -### Reactors (The "New" API) - -The release of v0.13 brought with it a new API called `IrcReactor` that enables easier multiserver -support and more graceful error handling. The general model is that you use the reactor to create -new `IrcClients`, register message handler functions, and finally block the thread to run the -clients with their respective handlers. Here's an example: - -```rust,no_run -extern crate irc; +The release of v0.14 replaced all existing APIs with one based on async/await. +```rust,no_run,edition2018 use irc::client::prelude::*; +use futures::prelude::*; -fn main() { +#[tokio::main] +async fn main() -> Result<(), failure::Error> { // We can also load the Config at runtime via Config::load("path/to/config.toml") let config = Config { nickname: Some("the-irc-crate".to_owned()), @@ -71,50 +66,16 @@ fn main() { ..Config::default() }; - let mut reactor = IrcReactor::new().unwrap(); - let client = reactor.prepare_client_and_connect(config).unwrap(); - client.identify().unwrap(); + let mut client = Client::from_config(config).await?; + client.identify()?; - reactor.register_client_with_handler(client, |client, message| { + let mut stream = client.stream()?; + + while let Some(message) = stream.next().await.transpose()? { print!("{}", message); - // And here we can do whatever we want with the messages. - Ok(()) - }); + } - reactor.run().unwrap(); -} -``` - - -### Direct Style (The "Old" API) - -The old API for connecting to an IRC server is still supported through the `IrcClient` type. It's -simpler for the most basic use case, but will panic upon encountering any sort of connection issues. -In general, it's recommended that users switch to the new API if possible. Nevertheless, here is an -example: - -```rust,no_run -extern crate irc; - -use std::default::Default; -use irc::client::prelude::*; - -fn main() { - // We can also load the Config at runtime via Config::load("path/to/config.toml") - let cfg = Config { - nickname: Some(format!("the-irc-crate")), - server: Some(format!("irc.example.com")), - channels: Some(vec![format!("#test")]), - .. Default::default() - }; - - let client = IrcClient::from_config(cfg).unwrap(); - client.identify().unwrap(); - - client.for_each_incoming(|message| { - print!("{}", message); - // And here we can do whatever we want with the messages. - }).unwrap() + Ok(()) } ``` diff --git a/src/bin/build-bot.rs b/examples/build-bot.rs similarity index 77% rename from src/bin/build-bot.rs rename to examples/build-bot.rs index bf464ed..7d85030 100644 --- a/src/bin/build-bot.rs +++ b/examples/build-bot.rs @@ -1,10 +1,12 @@ extern crate irc; +use futures::prelude::*; use irc::client::prelude::*; use std::default::Default; use std::env; -fn main() { +#[tokio::main] +async fn main() -> irc::error::Result<()> { let repository_slug = env::var("TRAVIS_REPO_SLUG").unwrap(); let branch = env::var("TRAVIS_BRANCH").unwrap(); let commit = env::var("TRAVIS_COMMIT").unwrap(); @@ -17,11 +19,13 @@ fn main() { ..Default::default() }; - let mut reactor = IrcReactor::new().unwrap(); - let client = reactor.prepare_client_and_connect(config).unwrap(); - client.identify().unwrap(); + let mut client = Client::from_config(config).await?; - reactor.register_client_with_handler(client, move |client, message| { + client.identify()?; + + let mut stream = client.stream()?; + + while let Some(message) = stream.next().await.transpose()? { match message.command { Command::Response(Response::RPL_ISUPPORT, _, _) => { client.send_privmsg( @@ -34,13 +38,12 @@ fn main() { commit_message ), )?; + client.send_quit("QUIT")?; } _ => (), - }; + } + } - Ok(()) - }); - - reactor.run().unwrap(); + Ok(()) } diff --git a/examples/convertconf.rs b/examples/convertconf.rs index 24af879..ade8dc1 100644 --- a/examples/convertconf.rs +++ b/examples/convertconf.rs @@ -1,7 +1,3 @@ -extern crate args; -extern crate getopts; -extern crate irc; - use std::env; use std::process::exit; @@ -34,18 +30,22 @@ fn main() { fn parse(input: &[String]) -> Result, ArgsError> { let mut args = Args::new(PROGRAM_NAME, PROGRAM_DESC); args.flag("h", "help", "Print the usage menu"); - args.option("i", - "input", - "The path to the input config", - "FILE", - Occur::Req, - None); - args.option("o", - "output", - "The path to output the new config to", - "FILE", - Occur::Req, - None); + args.option( + "i", + "input", + "The path to the input config", + "FILE", + Occur::Req, + None, + ); + args.option( + "o", + "output", + "The path to output the new config to", + "FILE", + Occur::Req, + None, + ); args.parse(input)?; @@ -55,8 +55,5 @@ fn parse(input: &[String]) -> Result, ArgsError> { return Ok(None); } - Ok(Some(( - args.value_of("input")?, - args.value_of("output")?, - ))) + Ok(Some((args.value_of("input")?, args.value_of("output")?))) } diff --git a/examples/multiserver.rs b/examples/multiserver.rs index 5c19bba..ecf69e2 100644 --- a/examples/multiserver.rs +++ b/examples/multiserver.rs @@ -1,10 +1,12 @@ extern crate irc; -use std::default::Default; -use irc::error; -use irc::client::prelude::*; +use futures::prelude::*; +use irc::{client::prelude::*, error}; + +#[tokio::main] +async fn main() -> irc::error::Result<()> { + env_logger::init(); -fn main() { let cfg1 = Config { nickname: Some("pickles".to_owned()), server: Some("irc.mozilla.org".to_owned()), @@ -20,32 +22,39 @@ fn main() { }; let configs = vec![cfg1, cfg2]; - - let mut reactor = IrcReactor::new().unwrap(); + let mut senders = Vec::new(); + let mut streams = Vec::new(); for config in configs { // Immediate errors like failure to resolve the server's domain or to establish any connection will // manifest here in the result of prepare_client_and_connect. - let client = reactor.prepare_client_and_connect(config).unwrap(); - client.identify().unwrap(); - // Here, we tell the reactor to setup this client for future handling (in run) using the specified - // handler function process_msg. - reactor.register_client_with_handler(client, process_msg); + let mut client = Client::from_config(config).await?; + client.identify()?; + + senders.push(client.sender()); + streams.push(client.stream()?); } - // Runtime errors like a dropped connection will manifest here in the result of run. - reactor.run().unwrap(); + loop { + let (message, index, _) = + futures::future::select_all(streams.iter_mut().map(|s| s.select_next_some())).await; + let message = message?; + let sender = &senders[index]; + process_msg(sender, message)?; + } } -fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { - print!("{}", message); +fn process_msg(sender: &Sender, message: Message) -> error::Result<()> { + // print!("{}", message); + match message.command { Command::PRIVMSG(ref target, ref msg) => { if msg.contains("pickles") { - client.send_privmsg(target, "Hi!")?; + sender.send_privmsg(target, "Hi!")?; } } _ => (), } + Ok(()) } diff --git a/examples/reactor.rs b/examples/reactor.rs deleted file mode 100644 index 3b0eb8c..0000000 --- a/examples/reactor.rs +++ /dev/null @@ -1,31 +0,0 @@ -extern crate irc; - -use std::default::Default; -use irc::client::prelude::*; - -// This example is meant to be a direct analogue to simple.rs using the reactor API. -fn main() { - let config = Config { - nickname: Some("pickles".to_owned()), - alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]), - server: Some("irc.mozilla.org".to_owned()), - channels: Some(vec!["#rust-spam".to_owned()]), - ..Default::default() - }; - - let mut reactor = IrcReactor::new().unwrap(); - let client = reactor.prepare_client_and_connect(config).unwrap(); - client.identify().unwrap(); - - reactor.register_client_with_handler(client, |client, message| { - print!("{}", message); - if let Command::PRIVMSG(ref target, ref msg) = message.command { - if msg.contains("pickles") { - client.send_privmsg(target, "Hi!")?; - } - } - Ok(()) - }); - - reactor.run().unwrap(); -} diff --git a/examples/reconnector.rs b/examples/reconnector.rs deleted file mode 100644 index b4fd0c1..0000000 --- a/examples/reconnector.rs +++ /dev/null @@ -1,57 +0,0 @@ -extern crate irc; - -use std::default::Default; -use irc::error; -use irc::client::prelude::*; - -fn main() { - let cfg1 = Config { - nickname: Some("pickles".to_owned()), - server: Some("irc.mozilla.org".to_owned()), - channels: Some(vec!["#rust-spam".to_owned()]), - ..Default::default() - }; - - let cfg2 = Config { - nickname: Some("bananas".to_owned()), - server: Some("irc.mozilla.org".to_owned()), - channels: Some(vec!["#rust-spam".to_owned()]), - ..Default::default() - }; - - let configs = vec![cfg1, cfg2]; - - let mut reactor = IrcReactor::new().unwrap(); - - loop { - let res = configs.iter().fold(Ok(()), |acc, config| { - acc.and( - reactor.prepare_client_and_connect(config.clone()).and_then(|client| { - client.identify().and(Ok(client)) - }).and_then(|client| { - reactor.register_client_with_handler(client, process_msg); - Ok(()) - }) - ) - }).and_then(|()| reactor.run()); - - match res { - // The connections ended normally (for example, they sent a QUIT message to the server). - Ok(_) => break, - // Something went wrong! We'll print the error, and restart the connections. - Err(e) => eprintln!("{}", e), - } - } -} - -fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { - print!("{}", message); - if let Command::PRIVMSG(ref target, ref msg) = message.command { - if msg.contains("pickles") { - client.send_privmsg(target, "Hi!")?; - } else if msg.contains("quit") { - client.send_quit("bye")?; - } - } - Ok(()) -} diff --git a/examples/repeater.rs b/examples/repeater.rs index 322d263..009616a 100644 --- a/examples/repeater.rs +++ b/examples/repeater.rs @@ -1,9 +1,8 @@ -extern crate irc; - -use std::default::Default; +use futures::prelude::*; use irc::client::prelude::*; -fn main() { +#[tokio::main] +async fn main() -> irc::error::Result<()> { let config = Config { nickname: Some("repeater".to_owned()), alt_nicks: Some(vec!["blaster".to_owned(), "smg".to_owned()]), @@ -15,11 +14,14 @@ fn main() { ..Default::default() }; - let client = IrcClient::from_config(config).unwrap(); - client.identify().unwrap(); + let mut client = Client::from_config(config).await?; + client.identify()?; + + let mut stream = client.stream()?; + + loop { + let message = stream.select_next_some().await?; - client.for_each_incoming(|message| { - print!("{}", message); if let Command::PRIVMSG(ref target, ref msg) = message.command { if msg.starts_with(&*client.current_nickname()) { let tokens: Vec<_> = msg.split(' ').collect(); @@ -29,12 +31,12 @@ fn main() { for _ in 0..count { client.send_privmsg( message.response_target().unwrap_or(target), - &msg[n..] - ).unwrap(); + &msg[n..], + )?; } } } } } - }).unwrap() + } } diff --git a/examples/simple.rs b/examples/simple.rs index 83bb777..d8e3ae4 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,9 +1,8 @@ -extern crate irc; - -use std::default::Default; +use futures::prelude::*; use irc::client::prelude::*; -fn main() { +#[tokio::main] +async fn main() -> irc::error::Result<()> { let config = Config { nickname: Some("pickles".to_owned()), alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]), @@ -12,15 +11,18 @@ fn main() { ..Default::default() }; - let client = IrcClient::from_config(config).unwrap(); - client.identify().unwrap(); + let mut client = Client::from_config(config).await?; + client.identify()?; + + let mut stream = client.stream()?; + + loop { + let message = stream.select_next_some().await?; - client.for_each_incoming(|message| { - print!("{}", message); if let Command::PRIVMSG(ref target, ref msg) = message.command { if msg.contains("pickles") { client.send_privmsg(target, "Hi!").unwrap(); } } - }).unwrap(); + } } diff --git a/examples/simple_ssl.rs b/examples/simple_ssl.rs index 201935a..f6d6d2e 100644 --- a/examples/simple_ssl.rs +++ b/examples/simple_ssl.rs @@ -1,9 +1,8 @@ -extern crate irc; - -use std::default::Default; +use futures::prelude::*; use irc::client::prelude::*; -fn main() { +#[tokio::main] +async fn main() -> irc::error::Result<()> { let config = Config { nickname: Some("pickles".to_owned()), server: Some("irc.mozilla.org".to_owned()), @@ -12,15 +11,17 @@ fn main() { ..Default::default() }; - let client = IrcClient::from_config(config).unwrap(); - client.identify().unwrap(); + let mut client = Client::from_config(config).await?; + let mut stream = client.stream()?; + let sender = client.sender(); + + loop { + let message = stream.select_next_some().await?; - client.for_each_incoming(|message| { - print!("{}", message); if let Command::PRIVMSG(ref target, ref msg) = message.command { if msg.contains("pickles") { - client.send_privmsg(target, "Hi!").unwrap(); + sender.send_privmsg(target, "Hi!")?; } } - }).unwrap(); + } } diff --git a/examples/tooter.rs b/examples/tooter.rs index 32bfefd..ab0736c 100644 --- a/examples/tooter.rs +++ b/examples/tooter.rs @@ -1,13 +1,10 @@ -extern crate irc; -extern crate tokio_timer; - -use std::default::Default; -use std::time::Duration; +use futures::prelude::*; use irc::client::prelude::*; -use irc::error::IrcError; +use std::time::Duration; // NOTE: this example is a conversion of `tweeter.rs` to an asynchronous style with `IrcReactor`. -fn main() { +#[tokio::main] +async fn main() -> irc::error::Result<()> { let config = Config { nickname: Some("mastodon".to_owned()), server: Some("irc.mozilla.org".to_owned()), @@ -15,37 +12,13 @@ fn main() { ..Default::default() }; - // We need to create a reactor first and foremost - let mut reactor = IrcReactor::new().unwrap(); - // and then create a client via its API. - let client = reactor.prepare_client_and_connect(config).unwrap(); - // Then, we identify - client.identify().unwrap(); - // and clone just as before. - let send_client = client.clone(); + let client = Client::from_config(config).await?; + let sender = client.sender(); - // Rather than spawn a thread that reads the messages separately, we register a handler with the - // reactor. just as in the original version, we don't do any real handling and instead just - // print the messages that are received. - reactor.register_client_with_handler(client, |_, message| { - print!("{}", message); - Ok(()) - }); + let mut interval = tokio::time::interval(Duration::from_secs(1)).fuse(); - // We construct an interval using a wheel timer from tokio_timer. This interval will fire every - // ten seconds (and is roughly accurate to the second). - let send_interval = tokio_timer::wheel() - .tick_duration(Duration::from_secs(1)) - .num_slots(256) - .build() - .interval(Duration::from_secs(10)); - - // And then spawn a new future that performs the given action each time it fires. - reactor.register_future(send_interval.map_err(IrcError::Timer).for_each(move |()| { - // Anything in here will happen every 10 seconds! - send_client.send_privmsg("#rust-spam", "AWOOOOOOOOOO") - })); - - // Then, on the main thread, we finally run the reactor which blocks the program indefinitely. - reactor.run().unwrap(); + loop { + let _ = interval.select_next_some().await; + sender.send_privmsg("#rust-spam", "AWOOOOOOOOOO")?; + } } diff --git a/examples/tweeter.rs b/examples/tweeter.rs index 824fab3..f6649d0 100644 --- a/examples/tweeter.rs +++ b/examples/tweeter.rs @@ -1,27 +1,31 @@ -extern crate irc; - -use std::default::Default; -use std::thread; -use std::time::Duration; +use futures::prelude::*; use irc::client::prelude::*; +use std::time::Duration; // NOTE: you can find an asynchronous version of this example with `IrcReactor` in `tooter.rs`. -fn main() { +#[tokio::main] +async fn main() -> irc::error::Result<()> { let config = Config { nickname: Some("pickles".to_owned()), server: Some("irc.mozilla.org".to_owned()), channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; - let client = IrcClient::from_config(config).unwrap(); - client.identify().unwrap(); - let client2 = client.clone(); - // Let's set up a loop that just prints the messages. - thread::spawn(move || { - client2.stream().map(|m| print!("{}", m)).wait().count(); - }); + + let mut client = Client::from_config(config).await?; + client.identify()?; + + let mut stream = client.stream()?; + let mut interval = tokio::time::interval(Duration::from_secs(10)).fuse(); + loop { - client.send_privmsg("#rust-spam", "TWEET TWEET").unwrap(); - thread::sleep(Duration::new(10, 0)); + futures::select! { + m = stream.select_next_some() => { + println!("{}", m?); + } + _ = interval.select_next_some() => { + client.send_privmsg("#rust-spam", "TWEET TWEET")?; + } + } } } diff --git a/irc-proto/Cargo.toml b/irc-proto/Cargo.toml index 30eb3c3..91463ca 100644 --- a/irc-proto/Cargo.toml +++ b/irc-proto/Cargo.toml @@ -13,12 +13,11 @@ repository = "https://github.com/aatxe/irc" travis-ci = { repository = "aatxe/irc" } [features] -default = ["tokio"] -tokio = ["tokio-codec", "tokio-io", "bytes"] +default = ["tokio", "tokio-util", "bytes"] [dependencies] -bytes = { version = "0.4", optional = true } +bytes = { version = "0.5", optional = true } encoding = "0.2" failure = "0.1" -tokio-codec = { version = "0.1", optional = true } -tokio-io = { version = "0.1", optional = true } +tokio-util = { version = "0.2.0", optional = true } +tokio = { version = "0.2.0", optional = true } diff --git a/irc-proto/src/chan.rs b/irc-proto/src/chan.rs index ad6cf05..3f437ea 100644 --- a/irc-proto/src/chan.rs +++ b/irc-proto/src/chan.rs @@ -8,10 +8,10 @@ pub trait ChannelExt { impl<'a> ChannelExt for &'a str { fn is_channel_name(&self) -> bool { - self.starts_with('#') || - self.starts_with('&') || - self.starts_with('+') || - self.starts_with('!') + self.starts_with('#') + || self.starts_with('&') + || self.starts_with('+') + || self.starts_with('!') } } diff --git a/irc-proto/src/colors.rs b/irc-proto/src/colors.rs index 0e8703d..239f7a0 100644 --- a/irc-proto/src/colors.rs +++ b/irc-proto/src/colors.rs @@ -15,13 +15,11 @@ struct Parser { /// An extension trait giving strings a function to strip IRC colors pub trait FormattedStringExt<'a> { - /// Returns true if the string contains color, bold, underline or italics fn is_formatted(&self) -> bool; /// Returns the string with all color, bold, underline and italics stripped fn strip_formatting(self) -> Cow<'a, str>; - } const FORMAT_CHARACTERS: &[char] = &[ @@ -54,7 +52,7 @@ fn strip_formatting(buf: &mut String) { impl Parser { fn new() -> Self { - Parser { + Parser { state: ParserState::Text, } } @@ -66,9 +64,7 @@ impl Parser { self.state = ColorCode; false } - Text => { - !FORMAT_CHARACTERS.contains(&cur) - } + Text => !FORMAT_CHARACTERS.contains(&cur), ColorCode if cur.is_digit(10) => { self.state = Foreground1(cur); false @@ -122,8 +118,8 @@ impl FormattedStringExt<'static> for String { #[cfg(test)] mod test { - use std::borrow::Cow; use colors::FormattedStringExt; + use std::borrow::Cow; macro_rules! test_formatted_string_ext { { $( $name:ident ( $($line:tt)* ), )* } => { diff --git a/irc-proto/src/command.rs b/irc-proto/src/command.rs index 387811a..b6fcbaf 100644 --- a/irc-proto/src/command.rs +++ b/irc-proto/src/command.rs @@ -1,8 +1,8 @@ //! Enumeration of all available client commands. use std::str::FromStr; -use error::MessageParseError; use chan::ChannelExt; +use error::MessageParseError; use mode::{ChannelMode, Mode, UserMode}; use response::Response; @@ -170,7 +170,12 @@ pub enum Command { // IRCv3 support /// CAP [*] COMMAND [*] :[param] - CAP(Option, CapSubCommand, Option, Option), + CAP( + Option, + CapSubCommand, + Option, + Option, + ), // IRCv3.1 extensions /// AUTHENTICATE data @@ -184,7 +189,12 @@ pub enum Command { // IRCv3.2 extensions /// METADATA target COMMAND [params] :[param] - METADATA(String, Option, Option>, Option), + METADATA( + String, + Option, + Option>, + Option, + ), /// MONITOR command [nicklist] MONITOR(String, Option), /// BATCH (+/-)reference-tag [type [params]] @@ -206,7 +216,6 @@ fn stringify(cmd: &str, args: &[&str], suffix: Option<&str>) -> String { Some(suffix) => format!("{}{}{} :{}", cmd, sp, args, suffix), None => format!("{}{}{}", cmd, sp, args), } - } impl<'a> From<&'a Command> for String { @@ -216,13 +225,15 @@ impl<'a> From<&'a Command> for String { Command::NICK(ref n) => stringify("NICK", &[], Some(n)), Command::USER(ref u, ref m, ref r) => stringify("USER", &[u, m, "*"], Some(r)), Command::OPER(ref u, ref p) => stringify("OPER", &[u], Some(p)), - Command::UserMODE(ref u, ref m) => { - format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| { + Command::UserMODE(ref u, ref m) => format!( + "MODE {}{}", + u, + m.iter().fold(String::new(), |mut acc, mode| { acc.push_str(" "); acc.push_str(&mode.to_string()); acc - })) - } + }) + ), Command::SERVICE(ref n, ref r, ref d, ref t, ref re, ref i) => { stringify("SERVICE", &[n, r, d, t, re], Some(i)) } @@ -235,13 +246,15 @@ impl<'a> From<&'a Command> for String { Command::JOIN(ref c, None, None) => stringify("JOIN", &[c], None), Command::PART(ref c, Some(ref m)) => stringify("PART", &[c], Some(m)), Command::PART(ref c, None) => stringify("PART", &[c], None), - Command::ChannelMODE(ref u, ref m) => { - format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| { + Command::ChannelMODE(ref u, ref m) => format!( + "MODE {}{}", + u, + m.iter().fold(String::new(), |mut acc, mode| { acc.push_str(" "); acc.push_str(&mode.to_string()); acc - })) - } + }) + ), Command::TOPIC(ref c, Some(ref t)) => stringify("TOPIC", &[c], Some(t)), Command::TOPIC(ref c, None) => stringify("TOPIC", &[c], None), Command::NAMES(Some(ref c), Some(ref t)) => stringify("NAMES", &[c], Some(t)), @@ -311,13 +324,11 @@ impl<'a> From<&'a Command> for String { Command::USERS(Some(ref t)) => stringify("USERS", &[], Some(t)), Command::USERS(None) => stringify("USERS", &[], None), Command::WALLOPS(ref t) => stringify("WALLOPS", &[], Some(t)), - Command::USERHOST(ref u) => { - stringify( - "USERHOST", - &u.iter().map(|s| &s[..]).collect::>(), - None, - ) - } + Command::USERHOST(ref u) => stringify( + "USERHOST", + &u.iter().map(|s| &s[..]).collect::>(), + None, + ), Command::ISON(ref u) => { stringify("ISON", &u.iter().map(|s| &s[..]).collect::>(), None) } @@ -369,92 +380,80 @@ impl<'a> From<&'a Command> for String { stringify("METADATA", &[&t[..], c.to_str()], None) } - Command::METADATA(ref t, Some(ref c), Some(ref a), Some(ref p)) => { - stringify( - "METADATA", - &vec![t, &c.to_str().to_owned()] - .iter() - .map(|s| &s[..]) - .chain(a.iter().map(|s| &s[..])) - .collect::>(), - Some(p), - ) - } - Command::METADATA(ref t, Some(ref c), Some(ref a), None) => { - stringify( - "METADATA", - &vec![t, &c.to_str().to_owned()] - .iter() - .map(|s| &s[..]) - .chain(a.iter().map(|s| &s[..])) - .collect::>(), - None, - ) - } + Command::METADATA(ref t, Some(ref c), Some(ref a), Some(ref p)) => stringify( + "METADATA", + &vec![t, &c.to_str().to_owned()] + .iter() + .map(|s| &s[..]) + .chain(a.iter().map(|s| &s[..])) + .collect::>(), + Some(p), + ), + Command::METADATA(ref t, Some(ref c), Some(ref a), None) => stringify( + "METADATA", + &vec![t, &c.to_str().to_owned()] + .iter() + .map(|s| &s[..]) + .chain(a.iter().map(|s| &s[..])) + .collect::>(), + None, + ), Command::METADATA(ref t, None, None, Some(ref p)) => { stringify("METADATA", &[t], Some(p)) } Command::METADATA(ref t, None, None, None) => stringify("METADATA", &[t], None), - Command::METADATA(ref t, None, Some(ref a), Some(ref p)) => { - stringify( - "METADATA", - &vec![t] - .iter() - .map(|s| &s[..]) - .chain(a.iter().map(|s| &s[..])) - .collect::>(), - Some(p), - ) - } - Command::METADATA(ref t, None, Some(ref a), None) => { - stringify( - "METADATA", - &vec![t] - .iter() - .map(|s| &s[..]) - .chain(a.iter().map(|s| &s[..])) - .collect::>(), - None, - ) - } + Command::METADATA(ref t, None, Some(ref a), Some(ref p)) => stringify( + "METADATA", + &vec![t] + .iter() + .map(|s| &s[..]) + .chain(a.iter().map(|s| &s[..])) + .collect::>(), + Some(p), + ), + Command::METADATA(ref t, None, Some(ref a), None) => stringify( + "METADATA", + &vec![t] + .iter() + .map(|s| &s[..]) + .chain(a.iter().map(|s| &s[..])) + .collect::>(), + None, + ), Command::MONITOR(ref c, Some(ref t)) => stringify("MONITOR", &[c, t], None), Command::MONITOR(ref c, None) => stringify("MONITOR", &[c], None), - Command::BATCH(ref t, Some(ref c), Some(ref a)) => { - stringify( - "BATCH", - &vec![t, &c.to_str().to_owned()] - .iter() - .map(|s| &s[..]) - .chain(a.iter().map(|s| &s[..])) - .collect::>(), - None, - ) - } + Command::BATCH(ref t, Some(ref c), Some(ref a)) => stringify( + "BATCH", + &vec![t, &c.to_str().to_owned()] + .iter() + .map(|s| &s[..]) + .chain(a.iter().map(|s| &s[..])) + .collect::>(), + None, + ), Command::BATCH(ref t, Some(ref c), None) => stringify("BATCH", &[t, c.to_str()], None), - Command::BATCH(ref t, None, Some(ref a)) => { - stringify( - "BATCH", - &vec![t] - .iter() - .map(|s| &s[..]) - .chain(a.iter().map(|s| &s[..])) - .collect::>(), - None, - ) - } + Command::BATCH(ref t, None, Some(ref a)) => stringify( + "BATCH", + &vec![t] + .iter() + .map(|s| &s[..]) + .chain(a.iter().map(|s| &s[..])) + .collect::>(), + None, + ), Command::BATCH(ref t, None, None) => stringify("BATCH", &[t], None), Command::CHGHOST(ref u, ref h) => stringify("CHGHOST", &[u, h], None), - Command::Response(ref resp, ref a, Some(ref s)) => { - stringify(&format!("{:03}", *resp as u16), - &a.iter().map(|s| &s[..]).collect::>(), - Some(s)) - } - Command::Response(ref resp, ref a, None) => { - stringify(&format!("{:03}", *resp as u16), - &a.iter().map(|s| &s[..]).collect::>(), - None) - } + Command::Response(ref resp, ref a, Some(ref s)) => stringify( + &format!("{:03}", *resp as u16), + &a.iter().map(|s| &s[..]).collect::>(), + Some(s), + ), + Command::Response(ref resp, ref a, None) => stringify( + &format!("{:03}", *resp as u16), + &a.iter().map(|s| &s[..]).collect::>(), + None, + ), Command::Raw(ref c, ref a, Some(ref s)) => { stringify(c, &a.iter().map(|s| &s[..]).collect::>(), Some(s)) } @@ -467,7 +466,11 @@ impl<'a> From<&'a Command> for String { impl Command { /// Constructs a new Command. - pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> Result { + pub fn new( + cmd: &str, + args: Vec<&str>, + suffix: Option<&str>, + ) -> Result { Ok(if cmd.eq_ignore_ascii_case("PASS") { match suffix { Some(suffix) => { @@ -539,13 +542,15 @@ impl Command { } else if cmd.eq_ignore_ascii_case("MODE") { match suffix { Some(suffix) => raw(cmd, args, Some(suffix)), - None => if args[0].is_channel_name() { - let arg = args[1..].join(" "); - Command::ChannelMODE(args[0].to_owned(), Mode::as_channel_modes(&arg)?) - } else { - let arg = args[1..].join(" "); - Command::UserMODE(args[0].to_owned(), Mode::as_user_modes(&arg)?) - }, + None => { + if args[0].is_channel_name() { + let arg = args[1..].join(" "); + Command::ChannelMODE(args[0].to_owned(), Mode::as_channel_modes(&arg)?) + } else { + let arg = args[1..].join(" "); + Command::UserMODE(args[0].to_owned(), Mode::as_user_modes(&arg)?) + } + } } } else if cmd.eq_ignore_ascii_case("SERVICE") { match suffix { @@ -1432,26 +1437,22 @@ impl Command { } else if args.len() == 2 { if let Ok(cmd) = args[0].parse() { match suffix { - Some(suffix) => { - Command::CAP( - None, - cmd, - Some(args[1].to_owned()), - Some(suffix.to_owned()), - ) - } + Some(suffix) => Command::CAP( + None, + cmd, + Some(args[1].to_owned()), + Some(suffix.to_owned()), + ), None => Command::CAP(None, cmd, Some(args[1].to_owned()), None), } } else if let Ok(cmd) = args[1].parse() { match suffix { - Some(suffix) => { - Command::CAP( - Some(args[0].to_owned()), - cmd, - None, - Some(suffix.to_owned()), - ) - } + Some(suffix) => Command::CAP( + Some(args[0].to_owned()), + cmd, + None, + Some(suffix.to_owned()), + ), None => Command::CAP(Some(args[0].to_owned()), cmd, None, None), } } else { @@ -1460,22 +1461,18 @@ impl Command { } else if args.len() == 3 { if let Ok(cmd) = args[1].parse() { match suffix { - Some(suffix) => { - Command::CAP( - Some(args[0].to_owned()), - cmd, - Some(args[2].to_owned()), - Some(suffix.to_owned()), - ) - } - None => { - Command::CAP( - Some(args[0].to_owned()), - cmd, - Some(args[2].to_owned()), - None, - ) - } + Some(suffix) => Command::CAP( + Some(args[0].to_owned()), + cmd, + Some(args[2].to_owned()), + Some(suffix.to_owned()), + ), + None => Command::CAP( + Some(args[0].to_owned()), + cmd, + Some(args[2].to_owned()), + None, + ), } } else { raw(cmd, args, suffix) @@ -1521,23 +1518,19 @@ impl Command { if args.len() == 2 { match suffix { Some(_) => raw(cmd, args, suffix), - None => { - match args[1].parse() { - Ok(c) => Command::METADATA(args[0].to_owned(), Some(c), None, None), - Err(_) => raw(cmd, args, suffix), - } - } + None => match args[1].parse() { + Ok(c) => Command::METADATA(args[0].to_owned(), Some(c), None, None), + Err(_) => raw(cmd, args, suffix), + }, } } else if args.len() > 2 { match args[1].parse() { - Ok(c) => { - Command::METADATA( - args[0].to_owned(), - Some(c), - Some(args.into_iter().skip(1).map(|s| s.to_owned()).collect()), - suffix.map(|s| s.to_owned()), - ) - } + Ok(c) => Command::METADATA( + args[0].to_owned(), + Some(c), + Some(args.into_iter().skip(1).map(|s| s.to_owned()).collect()), + suffix.map(|s| s.to_owned()), + ), Err(_) => { if args.len() == 3 && suffix.is_some() { Command::METADATA( @@ -1787,15 +1780,19 @@ impl FromStr for BatchSubCommand { #[cfg(test)] mod test { - use crate::Message; - use super::Response; use super::Command; + use super::Response; + use crate::Message; #[test] fn format_response() { - assert!(String::from(&Command::Response(Response::RPL_WELCOME, - vec!["foo".into()], - None)) == "001 foo"); + assert!( + String::from(&Command::Response( + Response::RPL_WELCOME, + vec!["foo".into()], + None + )) == "001 foo" + ); } #[test] @@ -1809,6 +1806,9 @@ mod test { #[test] fn parse_user_message() { let cmd = "USER a 0 * b".parse::().unwrap().command; - assert_eq!(Command::USER("a".to_string(), "0".to_string(), "b".to_string()), cmd); + assert_eq!( + Command::USER("a".to_string(), "0".to_string(), "b".to_string()), + cmd + ); } } diff --git a/irc-proto/src/error.rs b/irc-proto/src/error.rs index 3d3bd12..0343888 100644 --- a/irc-proto/src/error.rs +++ b/irc-proto/src/error.rs @@ -57,7 +57,7 @@ pub enum MessageParseError { cmd: &'static str, /// The invalid subcommand. sub: String, - } + }, } /// Errors that occur while parsing mode strings. diff --git a/irc-proto/src/irc.rs b/irc-proto/src/irc.rs index 84ef0ca..a11b556 100644 --- a/irc-proto/src/irc.rs +++ b/irc-proto/src/irc.rs @@ -1,6 +1,6 @@ //! Implementation of IRC codec for Tokio. use bytes::BytesMut; -use tokio_codec::{Decoder, Encoder}; +use tokio_util::codec::{Decoder, Encoder}; use error; use line::LineCodec; @@ -31,7 +31,6 @@ impl IrcCodec { } data } - } impl Decoder for IrcCodec { @@ -39,9 +38,9 @@ impl Decoder for IrcCodec { type Error = error::ProtocolError; fn decode(&mut self, src: &mut BytesMut) -> error::Result> { - self.inner.decode(src).and_then(|res| { - res.map_or(Ok(None), |msg| msg.parse::().map(Some)) - }) + self.inner + .decode(src) + .and_then(|res| res.map_or(Ok(None), |msg| msg.parse::().map(Some))) } } @@ -49,7 +48,6 @@ impl Encoder for IrcCodec { type Item = Message; type Error = error::ProtocolError; - fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> { self.inner.encode(IrcCodec::sanitize(msg.to_string()), dst) } diff --git a/irc-proto/src/lib.rs b/irc-proto/src/lib.rs index 09e5bba..fe764f9 100644 --- a/irc-proto/src/lib.rs +++ b/irc-proto/src/lib.rs @@ -8,9 +8,9 @@ extern crate encoding; #[macro_use] extern crate failure; #[cfg(feature = "tokio")] -extern crate tokio_codec; -#[cfg(feature = "tokio")] -extern crate tokio_io; +extern crate tokio; +#[cfg(feature = "tokio-util")] +extern crate tokio_util; pub mod caps; pub mod chan; diff --git a/irc-proto/src/line.rs b/irc-proto/src/line.rs index 0b26a35..3c75128 100644 --- a/irc-proto/src/line.rs +++ b/irc-proto/src/line.rs @@ -3,9 +3,9 @@ use std::io; use bytes::BytesMut; -use encoding::{DecoderTrap, EncoderTrap, EncodingRef}; use encoding::label::encoding_from_whatwg_label; -use tokio_io::codec::{Decoder, Encoder}; +use encoding::{DecoderTrap, EncoderTrap, EncodingRef}; +use tokio_util::codec::{Decoder, Encoder}; use error; @@ -19,11 +19,17 @@ impl LineCodec { /// Creates a new instance of LineCodec from the specified encoding. pub fn new(label: &str) -> error::Result { encoding_from_whatwg_label(label) - .map(|enc| LineCodec { encoding: enc, next_index: 0 }) - .ok_or_else(|| io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Attempted to use unknown codec {}.", label)[..], - ).into()) + .map(|enc| LineCodec { + encoding: enc, + next_index: 0, + }) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + &format!("Attempted to use unknown codec {}.", label)[..], + ) + .into() + }) } } @@ -42,12 +48,11 @@ impl Decoder for LineCodec { // Decode the line using the codec's encoding. match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) { Ok(data) => Ok(Some(data)), - Err(data) => Err( - io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Failed to decode {} as {}.", data, self.encoding.name())[..], - ).into(), - ), + Err(data) => Err(io::Error::new( + io::ErrorKind::InvalidInput, + &format!("Failed to decode {} as {}.", data, self.encoding.name())[..], + ) + .into()), } } else { // Set the search start index to the current length since we know that none of the @@ -64,13 +69,15 @@ impl Encoder for LineCodec { fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> { // Encode the message using the codec's encoding. - let data: error::Result> = self.encoding + let data: error::Result> = self + .encoding .encode(&msg, EncoderTrap::Replace) .map_err(|data| { io::Error::new( io::ErrorKind::InvalidInput, &format!("Failed to encode {} as {}.", data, self.encoding.name())[..], - ).into() + ) + .into() }); // Write the encoded message to the output buffer. diff --git a/irc-proto/src/message.rs b/irc-proto/src/message.rs index b2648d7..e121ca8 100644 --- a/irc-proto/src/message.rs +++ b/irc-proto/src/message.rs @@ -9,7 +9,6 @@ use error; use error::{MessageParseError, ProtocolError}; use prefix::Prefix; - /// A data structure representing an IRC message according to the protocol specification. It /// consists of a collection of IRCv3 tags, a prefix (describing the source of the message), and /// the protocol command. If the command is unknown, it is treated as a special raw command that @@ -85,7 +84,7 @@ impl Message { // ::= self.prefix.as_ref().and_then(|p| match p { Prefix::Nickname(name, _, _) => Some(&name[..]), - _ => None + _ => None, }) } @@ -112,7 +111,7 @@ impl Message { match self.command { Command::PRIVMSG(ref target, _) if target.is_channel_name() => Some(target), Command::NOTICE(ref target, _) if target.is_channel_name() => Some(target), - _ => self.source_nickname() + _ => self.source_nickname(), } } @@ -172,7 +171,7 @@ impl FromStr for Message { return Err(ProtocolError::InvalidMessage { string: s.to_owned(), cause: MessageParseError::EmptyMessage, - }) + }); } let mut state = s; @@ -210,10 +209,13 @@ impl FromStr for Message { "\n" } else { "" - }.len(); + } + .len(); let suffix = if state.contains(" :") { - let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - line_ending_len]); + let suffix = state + .find(" :") + .map(|i| &state[i + 2..state.len() - line_ending_len]); state = state.find(" :").map_or("", |i| &state[..i + 1]); suffix } else { @@ -227,16 +229,18 @@ impl FromStr for Message { cmd } // If there's no arguments but the "command" starts with colon, it's not a command. - None if state.starts_with(':') => return Err(ProtocolError::InvalidMessage { - string: s.to_owned(), - cause: MessageParseError::InvalidCommand, - }), + None if state.starts_with(':') => { + return Err(ProtocolError::InvalidMessage { + string: s.to_owned(), + cause: MessageParseError::InvalidCommand, + }) + } // If there's no arguments following the command, the rest of the state is the command. None => { let cmd = state; state = ""; cmd - }, + } }; let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect(); @@ -272,7 +276,7 @@ pub struct Tag(pub String, pub Option); #[cfg(test)] mod test { use super::{Message, Tag}; - use command::Command::{PRIVMSG, QUIT, Raw}; + use command::Command::{Raw, PRIVMSG, QUIT}; #[test] fn new() { @@ -398,7 +402,7 @@ mod test { }; assert_eq!( "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \ - tags!\r\n" + tags!\r\n" .parse::() .unwrap(), message @@ -428,8 +432,9 @@ mod test { #[test] fn from_and_to_string() { - let message = "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \ - tags!\r\n"; + let message = + "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \ + tags!\r\n"; assert_eq!(message.parse::().unwrap().to_string(), message); } diff --git a/irc-proto/src/mode.rs b/irc-proto/src/mode.rs index dff3f5b..4891776 100644 --- a/irc-proto/src/mode.rs +++ b/irc-proto/src/mode.rs @@ -1,10 +1,10 @@ //! A module defining an API for IRC user and channel modes. use std::fmt; +use command::Command; use error::MessageParseError; use error::MessageParseError::InvalidModeString; use error::ModeParseError::*; -use command::Command; /// A marker trait for different kinds of Modes. pub trait ModeType: fmt::Display + fmt::Debug + Clone + PartialEq { @@ -71,17 +71,21 @@ impl fmt::Display for UserMode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::UserMode::*; - write!(f, "{}", match *self { - Away => 'a', - Invisible => 'i', - Wallops => 'w', - Restricted => 'r', - Oper => 'o', - LocalOper => 'O', - ServerNotices => 's', - MaskedHost => 'x', - Unknown(c) => c, - }) + write!( + f, + "{}", + match *self { + Away => 'a', + Invisible => 'i', + Wallops => 'w', + Restricted => 'r', + Oper => 'o', + LocalOper => 'O', + ServerNotices => 's', + MaskedHost => 'x', + Unknown(c) => c, + } + ) } } @@ -135,8 +139,8 @@ impl ModeType for ChannelMode { use self::ChannelMode::*; match *self { - Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop | - Voice => true, + Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop + | Voice => true, _ => false, } } @@ -172,25 +176,29 @@ impl fmt::Display for ChannelMode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::ChannelMode::*; - write!(f, "{}", match *self { - Ban => 'b', - Exception => 'e', - Limit => 'l', - InviteOnly => 'i', - InviteException => 'I', - Key => 'k', - Moderated => 'm', - RegisteredOnly => 'r', - Secret => 's', - ProtectedTopic => 't', - NoExternalMessages => 'n', - Founder => 'q', - Admin => 'a', - Oper => 'o', - Halfop => 'h', - Voice => 'v', - Unknown(c) => c, - }) + write!( + f, + "{}", + match *self { + Ban => 'b', + Exception => 'e', + Limit => 'l', + InviteOnly => 'i', + InviteException => 'I', + Key => 'k', + Moderated => 'm', + RegisteredOnly => 'r', + Secret => 's', + ProtectedTopic => 't', + NoExternalMessages => 'n', + Founder => 'q', + Admin => 'a', + Oper => 'o', + Halfop => 'h', + Voice => 'v', + Unknown(c) => c, + } + ) } } @@ -257,14 +265,18 @@ impl Mode { let init = match chars.next() { Some('+') => Plus, Some('-') => Minus, - Some(c) => return Err(InvalidModeString { - string: s.to_owned(), - cause: InvalidModeModifier { modifier: c }, - }), - None => return Err(InvalidModeString { - string: s.to_owned(), - cause: MissingModeModifier, - }), + Some(c) => { + return Err(InvalidModeString { + string: s.to_owned(), + cause: InvalidModeModifier { modifier: c }, + }) + } + None => { + return Err(InvalidModeString { + string: s.to_owned(), + cause: MissingModeModifier, + }) + } }; for c in chars { @@ -303,14 +315,18 @@ impl Mode { let init = match chars.next() { Some('+') => Plus, Some('-') => Minus, - Some(c) => return Err(InvalidModeString { - string: s.to_owned(), - cause: InvalidModeModifier { modifier: c }, - }), - None => return Err(InvalidModeString { - string: s.to_owned(), - cause: MissingModeModifier, - }), + Some(c) => { + return Err(InvalidModeString { + string: s.to_owned(), + cause: InvalidModeModifier { modifier: c }, + }) + } + None => { + return Err(InvalidModeString { + string: s.to_owned(), + cause: MissingModeModifier, + }) + } }; for c in chars { diff --git a/irc-proto/src/prefix.rs b/irc-proto/src/prefix.rs index 9de076e..3d5dbab 100644 --- a/irc-proto/src/prefix.rs +++ b/irc-proto/src/prefix.rs @@ -1,6 +1,6 @@ //! A module providing an enum for a message prefix. -use std::str::FromStr; use std::fmt; +use std::str::FromStr; /// The Prefix indicates "the true origin of the message", according to the server. #[derive(Clone, Eq, PartialEq, Debug)] @@ -50,12 +50,12 @@ impl Prefix { '!' if active == Active::Name => { is_server = false; active = Active::User; - }, + } '@' if active != Active::Host => { is_server = false; active = Active::Host; - }, + } _ => { // Push onto the active buffer @@ -63,7 +63,8 @@ impl Prefix { Active::Name => &mut name, Active::User => &mut user, Active::Host => &mut host, - }.push(c) + } + .push(c) } } } @@ -109,7 +110,7 @@ impl<'a> From<&'a str> for Prefix { #[cfg(test)] mod test { - use super::Prefix::{self, ServerName, Nickname}; + use super::Prefix::{self, Nickname, ServerName}; // Checks that str -> parsed -> Display doesn't lose data fn test_parse(s: &str) -> Prefix { @@ -139,10 +140,7 @@ mod test { #[test] fn parse_host() { - assert_eq!( - test_parse("host.tld"), - ServerName("host.tld".into()) - ) + assert_eq!(test_parse("host.tld"), ServerName("host.tld".into())) } #[test] diff --git a/irc-proto/src/response.rs b/irc-proto/src/response.rs index c42c171..de0376d 100644 --- a/irc-proto/src/response.rs +++ b/irc-proto/src/response.rs @@ -52,7 +52,7 @@ make_response! { RPL_BOUNCE = 10, /// Undefined format. (Source: Modern) /// - /// RPL_NONE is a dummy numeric. It does not have a defined use nor format. + /// RPL_NONE is a dummy numeric. It does not have a defined use nor format. RPL_NONE = 300, /// `302 :*1 *( " " )` (Source: RFC2812) RPL_USERHOST = 302, diff --git a/mktestconfig.sh b/mktestconfig.sh deleted file mode 100755 index f1d1d6e..0000000 --- a/mktestconfig.sh +++ /dev/null @@ -1,3 +0,0 @@ -echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.test.net\",\"port\": 6667,\"use_ssl\": false,\"encoding\": \"UTF-8\",\"channels\": [\"#test\", \"#test2\"],\"umodes\": \"+BR\",\"options\": {}}" > client_config.json -cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.toml -cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.yaml diff --git a/src/client/conn.rs b/src/client/conn.rs index f49b0c0..5a4ff75 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -1,31 +1,35 @@ //! A module providing IRC connections for use by `IrcServer`s. -use std::fs::File; -use std::fmt; -use std::io::Read; - -use encoding::EncoderTrap; -use encoding::label::encoding_from_whatwg_label; -use futures::{Async, Poll, Future, Sink, StartSend, Stream}; -use native_tls::{Certificate, TlsConnector, Identity}; -use tokio_codec::Decoder; -use tokio::net::{TcpStream}; -use tokio::net::tcp::ConnectFuture; -use tokio_mockstream::MockStream; +use futures_channel::mpsc::UnboundedSender; +use futures_util::{sink::Sink, stream::Stream}; +use native_tls::{Certificate, Identity, TlsConnector}; +use std::{ + fmt, + fs::File, + io::Read, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::net::TcpStream; use tokio_tls::{self, TlsStream}; +use tokio_util::codec::Decoder; -use error; -use client::data::Config; -use client::transport::{IrcTransport, LogView, Logged}; -use proto::{IrcCodec, Message}; +use crate::{ + client::{ + data::Config, + transport::{LogView, Logged, Transport}, + }, + error, + proto::{IrcCodec, Message}, +}; /// An IRC connection used internally by `IrcServer`. pub enum Connection { #[doc(hidden)] - Unsecured(IrcTransport), + Unsecured(Transport), #[doc(hidden)] - Secured(IrcTransport>), + Secured(Transport>), #[doc(hidden)] - Mock(Logged), + Mock(Logged), } impl fmt::Debug for Connection { @@ -42,102 +46,49 @@ impl fmt::Debug for Connection { } } -/// A convenient type alias representing the `TlsStream` future. -type TlsFuture = Box> + Send>; - -/// A future representing an eventual `Connection`. -pub enum ConnectionFuture { - #[doc(hidden)] - Unsecured(Config, ConnectFuture), - #[doc(hidden)] - Secured(Config, TlsFuture), - #[doc(hidden)] - Mock(Config), -} - -impl fmt::Debug for ConnectionFuture { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}({:?}, ...)", - match *self { - ConnectionFuture::Unsecured(_, _) => "ConnectionFuture::Unsecured", - ConnectionFuture::Secured(_, _) => "ConnectionFuture::Secured", - ConnectionFuture::Mock(_) => "ConnectionFuture::Mock", - }, - match *self { - ConnectionFuture::Unsecured(ref cfg, _) | - ConnectionFuture::Secured(ref cfg, _) | - ConnectionFuture::Mock(ref cfg) => cfg, - } - ) - } -} - -impl Future for ConnectionFuture { - type Item = Connection; - type Error = error::IrcError; - - fn poll(&mut self) -> Poll { - match *self { - ConnectionFuture::Unsecured(ref config, ref mut inner) => { - let stream = try_ready!(inner.poll()); - let framed = IrcCodec::new(config.encoding())?.framed(stream); - let transport = IrcTransport::new(&config, framed); - - Ok(Async::Ready(Connection::Unsecured(transport))) - } - ConnectionFuture::Secured(ref config, ref mut inner) => { - let stream = try_ready!(inner.poll()); - let framed = IrcCodec::new(config.encoding())?.framed(stream); - let transport = IrcTransport::new(&config, framed); - - Ok(Async::Ready(Connection::Secured(transport))) - } - ConnectionFuture::Mock(ref config) => { - let enc: error::Result<_> = encoding_from_whatwg_label( - config.encoding() - ).ok_or_else(|| error::IrcError::UnknownCodec { - codec: config.encoding().to_owned(), - }); - let encoding = enc?; - let init_str = config.mock_initial_value(); - let initial: error::Result<_> = { - encoding.encode(init_str, EncoderTrap::Replace).map_err(|data| { - error::IrcError::CodecFailed { - codec: encoding.name(), - data: data.into_owned(), - } - }) - }; - - let stream = MockStream::new(&initial?); - let framed = IrcCodec::new(config.encoding())?.framed(stream); - let transport = IrcTransport::new(&config, framed); - - Ok(Async::Ready(Connection::Mock(Logged::wrap(transport)))) - } - } - } -} - impl Connection { /// Creates a new `Connection` using the specified `Config` - pub fn new(config: Config) -> error::Result { + pub(crate) async fn new( + config: &Config, + tx: UnboundedSender, + ) -> error::Result { if config.use_mock_connection() { - Ok(ConnectionFuture::Mock(config)) - } else if config.use_ssl() { + use encoding::{label::encoding_from_whatwg_label, EncoderTrap}; + + let encoding = encoding_from_whatwg_label(config.encoding()).ok_or_else(|| { + error::Error::UnknownCodec { + codec: config.encoding().to_owned(), + } + })?; + + let init_str = config.mock_initial_value(); + let initial = encoding + .encode(init_str, EncoderTrap::Replace) + .map_err(|data| error::Error::CodecFailed { + codec: encoding.name(), + data: data.into_owned(), + })?; + + let stream = crate::client::mock::MockStream::new(&initial); + let framed = IrcCodec::new(config.encoding())?.framed(stream); + let transport = Transport::new(&config, framed, tx); + return Ok(Connection::Mock(Logged::wrap(transport))); + } + + if config.use_ssl() { let domain = format!("{}", config.server()?); - info!("Connecting via SSL to {}.", domain); + log::info!("Connecting via SSL to {}.", domain); let mut builder = TlsConnector::builder(); + if let Some(cert_path) = config.cert_path() { let mut file = File::open(cert_path)?; let mut cert_data = vec![]; file.read_to_end(&mut cert_data)?; let cert = Certificate::from_der(&cert_data)?; builder.add_root_certificate(cert); - info!("Added {} to trusted certificates.", cert_path); + log::info!("Added {} to trusted certificates.", cert_path); } + if let Some(client_cert_path) = config.client_cert_path() { let client_cert_pass = config.client_cert_pass(); let mut file = File::open(client_cert_path)?; @@ -145,25 +96,27 @@ impl Connection { file.read_to_end(&mut client_cert_data)?; let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?; builder.identity(pkcs12_archive); - info!("Using {} for client certificate authentication.", client_cert_path); + log::info!( + "Using {} for client certificate authentication.", + client_cert_path + ); } let connector: tokio_tls::TlsConnector = builder.build()?.into(); - let stream = Box::new(TcpStream::connect(&config.socket_addr()?).map_err(|e| { - let res: error::IrcError = e.into(); - res - }).and_then(move |socket| { - connector.connect(&domain, socket).map_err( - |e| e.into(), - ) - })); - Ok(ConnectionFuture::Secured(config, stream)) + + let socket = TcpStream::connect(&config.socket_addr()?).await?; + let stream = connector.connect(&domain, socket).await?; + let framed = IrcCodec::new(config.encoding())?.framed(stream); + let transport = Transport::new(&config, framed, tx); + + Ok(Connection::Secured(transport)) } else { - info!("Connecting to {}.", config.server()?); + log::info!("Connecting to {}.", config.server()?); let addr = config.socket_addr()?; - Ok(ConnectionFuture::Unsecured( - config, - TcpStream::connect(&addr), - )) + let stream = TcpStream::connect(&addr).await?; + let framed = IrcCodec::new(config.encoding())?.framed(stream); + let transport = Transport::new(&config, framed, tx); + + Ok(Connection::Unsecured(transport)) } } @@ -178,35 +131,49 @@ impl Connection { } impl Stream for Connection { - type Item = Message; - type Error = error::IrcError; + type Item = error::Result; - fn poll(&mut self) -> Poll, Self::Error> { - match *self { - Connection::Unsecured(ref mut inner) => inner.poll(), - Connection::Secured(ref mut inner) => inner.poll(), - Connection::Mock(ref mut inner) => inner.poll(), + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match &mut *self { + Connection::Unsecured(inner) => Pin::new(inner).poll_next(cx), + Connection::Secured(inner) => Pin::new(inner).poll_next(cx), + Connection::Mock(inner) => Pin::new(inner).poll_next(cx), } } } -impl Sink for Connection { - type SinkItem = Message; - type SinkError = error::IrcError; +impl Sink for Connection { + type Error = error::Error; - fn start_send(&mut self, item: Self::SinkItem) -> StartSend { - match *self { - Connection::Unsecured(ref mut inner) => inner.start_send(item), - Connection::Secured(ref mut inner) => inner.start_send(item), - Connection::Mock(ref mut inner) => inner.start_send(item), + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match &mut *self { + Connection::Unsecured(inner) => Pin::new(inner).poll_ready(cx), + Connection::Secured(inner) => Pin::new(inner).poll_ready(cx), + Connection::Mock(inner) => Pin::new(inner).poll_ready(cx), } } - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - match *self { - Connection::Unsecured(ref mut inner) => inner.poll_complete(), - Connection::Secured(ref mut inner) => inner.poll_complete(), - Connection::Mock(ref mut inner) => inner.poll_complete(), + fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + match &mut *self { + Connection::Unsecured(inner) => Pin::new(inner).start_send(item), + Connection::Secured(inner) => Pin::new(inner).start_send(item), + Connection::Mock(inner) => Pin::new(inner).start_send(item), + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match &mut *self { + Connection::Unsecured(inner) => Pin::new(inner).poll_flush(cx), + Connection::Secured(inner) => Pin::new(inner).poll_flush(cx), + Connection::Mock(inner) => Pin::new(inner).poll_flush(cx), + } + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match &mut *self { + Connection::Unsecured(inner) => Pin::new(inner).poll_close(cx), + Connection::Secured(inner) => Pin::new(inner).poll_close(cx), + Connection::Mock(inner) => Pin::new(inner).poll_close(cx), } } } diff --git a/src/client/data/client_config.json b/src/client/data/client_config.json new file mode 100644 index 0000000..9d820d5 --- /dev/null +++ b/src/client/data/client_config.json @@ -0,0 +1,19 @@ +{ + "owners": [ + "test" + ], + "nickname": "test", + "username": "test", + "realname": "test", + "password": "", + "server": "irc.test.net", + "port": 6667, + "use_ssl": false, + "encoding": "UTF-8", + "channels": [ + "#test", + "#test2" + ], + "umodes": "+BR", + "options": {} +} \ No newline at end of file diff --git a/src/client/data/client_config.toml b/src/client/data/client_config.toml new file mode 100644 index 0000000..f253250 --- /dev/null +++ b/src/client/data/client_config.toml @@ -0,0 +1,13 @@ +owners = ["test"] +nickname = "test" +username = "test" +realname = "test" +server = "irc.test.net" +port = 6667 +password = "" +use_ssl = false +encoding = "UTF-8" +channels = ["#test", "#test2"] +umodes = "+BR" + +[options] \ No newline at end of file diff --git a/src/client/data/client_config.yaml b/src/client/data/client_config.yaml new file mode 100644 index 0000000..aaeef08 --- /dev/null +++ b/src/client/data/client_config.yaml @@ -0,0 +1,16 @@ +--- +owners: + - test +nickname: test +username: test +realname: test +server: irc.test.net +port: 6667 +password: "" +use_ssl: false +encoding: UTF-8 +channels: + - "#test" + - "#test2" +umodes: +BR +options: {} \ No newline at end of file diff --git a/src/client/data/config.rs b/src/client/data/config.rs index d7e5575..ba01b1c 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -1,10 +1,12 @@ //! JSON configuration files using serde -use std::borrow::ToOwned; -use std::collections::HashMap; -use std::fs::File; -use std::io::prelude::*; -use std::net::{SocketAddr, ToSocketAddrs}; -use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fs::File, + io::prelude::*, + net::{SocketAddr, ToSocketAddrs}, + path::{Path, PathBuf}, +}; #[cfg(feature = "json")] use serde_json; @@ -13,10 +15,10 @@ use serde_yaml; #[cfg(feature = "toml")] use toml; +use crate::error::Error::InvalidConfig; #[cfg(feature = "toml")] -use error::TomlError; -use error::{ConfigError, Result}; -use error::IrcError::InvalidConfig; +use crate::error::TomlError; +use crate::error::{ConfigError, Result}; /// Configuration for IRC clients. /// @@ -153,9 +155,10 @@ impl Config { } fn path(&self) -> String { - self.path.as_ref().map(|buf| buf.to_string_lossy().into_owned()).unwrap_or_else(|| { - "".to_owned() - }) + self.path + .as_ref() + .map(|buf| buf.to_string_lossy().into_owned()) + .unwrap_or_else(|| "".to_owned()) } /// Loads a configuration from the desired path. This will use the file extension to detect @@ -182,68 +185,54 @@ impl Config { }), }; - res.map(|config| { - config.with_path(path) - }) + res.map(|config| config.with_path(path)) } #[cfg(feature = "json")] - fn load_json>(path: &P, data: &str) -> Result { - serde_json::from_str(&data[..]).map_err(|e| { - InvalidConfig { - path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::InvalidJson(e), - } + fn load_json>(path: P, data: &str) -> Result { + serde_json::from_str(data).map_err(|e| InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidJson(e), }) } #[cfg(not(feature = "json"))] - fn load_json>(path: &P, _: &str) -> Result { + fn load_json>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::ConfigFormatDisabled { - format: "JSON" - } + cause: ConfigError::ConfigFormatDisabled { format: "JSON" }, }) } #[cfg(feature = "toml")] - fn load_toml>(path: &P, data: &str) -> Result { - toml::from_str(&data[..]).map_err(|e| { - InvalidConfig { - path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::InvalidToml(TomlError::Read(e)), - } + fn load_toml>(path: P, data: &str) -> Result { + toml::from_str(data).map_err(|e| InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidToml(TomlError::Read(e)), }) } #[cfg(not(feature = "toml"))] - fn load_toml>(path: &P, _: &str) -> Result { + fn load_toml>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::ConfigFormatDisabled { - format: "TOML" - } + cause: ConfigError::ConfigFormatDisabled { format: "TOML" }, }) } #[cfg(feature = "yaml")] - fn load_yaml>(path: &P, data: &str) -> Result { - serde_yaml::from_str(&data[..]).map_err(|e| { - InvalidConfig { - path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::InvalidYaml(e), - } + fn load_yaml>(path: P, data: &str) -> Result { + serde_yaml::from_str(data).map_err(|e| InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidYaml(e), }) } #[cfg(not(feature = "yaml"))] - fn load_yaml>(path: &P, _: &str) -> Result { + fn load_yaml>(path: P, _: &str) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::ConfigFormatDisabled { - format: "YAML" - } + cause: ConfigError::ConfigFormatDisabled { format: "YAML" }, }) } @@ -257,16 +246,20 @@ impl Config { Some("json") => self.save_json(&path)?, Some("toml") => self.save_toml(&path)?, Some("yaml") | Some("yml") => self.save_yaml(&path)?, - Some(ext) => return Err(InvalidConfig { - path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::UnknownConfigFormat { - format: ext.to_owned(), - }, - }), - None => return Err(InvalidConfig { - path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::MissingExtension, - }), + Some(ext) => { + return Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::UnknownConfigFormat { + format: ext.to_owned(), + }, + }) + } + None => { + return Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::MissingExtension, + }) + } }; file.write_all(data.as_bytes())?; self.path = Some(path.as_ref().to_owned()); @@ -275,11 +268,9 @@ impl Config { #[cfg(feature = "json")] fn save_json>(&self, path: &P) -> Result { - serde_json::to_string(self).map_err(|e| { - InvalidConfig { - path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::InvalidJson(e), - } + serde_json::to_string(self).map_err(|e| InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidJson(e), }) } @@ -287,19 +278,15 @@ impl Config { fn save_json>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::ConfigFormatDisabled { - format: "JSON" - } + cause: ConfigError::ConfigFormatDisabled { format: "JSON" }, }) } #[cfg(feature = "toml")] fn save_toml>(&self, path: &P) -> Result { - toml::to_string(self).map_err(|e| { - InvalidConfig { - path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::InvalidToml(TomlError::Write(e)), - } + toml::to_string(self).map_err(|e| InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidToml(TomlError::Write(e)), }) } @@ -307,19 +294,15 @@ impl Config { fn save_toml>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::ConfigFormatDisabled { - format: "TOML" - } + cause: ConfigError::ConfigFormatDisabled { format: "TOML" }, }) } #[cfg(feature = "yaml")] fn save_yaml>(&self, path: &P) -> Result { - serde_yaml::to_string(self).map_err(|e| { - InvalidConfig { - path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::InvalidYaml(e), - } + serde_yaml::to_string(self).map_err(|e| InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidYaml(e), }) } @@ -327,9 +310,7 @@ impl Config { fn save_yaml>(&self, path: &P) -> Result { Err(InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(), - cause: ConfigError::ConfigFormatDisabled { - format: "YAML" - } + cause: ConfigError::ConfigFormatDisabled { format: "YAML" }, }) } @@ -343,12 +324,13 @@ impl Config { /// Gets the nickname specified in the configuration. pub fn nickname(&self) -> Result<&str> { - self.nickname.as_ref().map(|s| &s[..]).ok_or_else(|| { - InvalidConfig { + self.nickname + .as_ref() + .map(|s| &s[..]) + .ok_or_else(|| InvalidConfig { path: self.path(), cause: ConfigError::NicknameNotSpecified, - } - }) + }) } /// Gets the bot's nickserv password specified in the configuration. @@ -360,48 +342,52 @@ 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()) } - /// Gets the username specified in the configuration. /// This defaults to the user's nickname when not specified. pub fn username(&self) -> &str { - self.username.as_ref().map_or(self.nickname().unwrap_or("user"), |s| &s) + self.username + .as_ref() + .map_or(self.nickname().unwrap_or("user"), |s| &s) } /// Gets the real name specified in the configuration. /// This defaults to the user's nickname when not specified. pub fn real_name(&self) -> &str { - self.realname.as_ref().map_or(self.nickname().unwrap_or("irc"), |s| &s) + self.realname + .as_ref() + .map_or(self.nickname().unwrap_or("irc"), |s| &s) } /// Gets the address of the server specified in the configuration. pub fn server(&self) -> Result<&str> { - self.server.as_ref().map(|s| &s[..]).ok_or_else(|| { - InvalidConfig { + self.server + .as_ref() + .map(|s| &s[..]) + .ok_or_else(|| InvalidConfig { path: self.path(), cause: ConfigError::ServerNotSpecified, - } - }) + }) } /// Gets the port of the server specified in the configuration. /// This defaults to 6667 (or 6697 if use_ssl is specified as true) when not specified. pub fn port(&self) -> u16 { - self.port.as_ref().cloned().unwrap_or(if self.use_ssl() { - 6697 - } else { - 6667 - }) + self.port + .as_ref() + .cloned() + .unwrap_or(if self.use_ssl() { 6697 } else { 6667 }) } /// Gets the server and port as a `SocketAddr`. /// This panics when server is not specified or the address is malformed. pub fn socket_addr(&self) -> Result { - format!("{}:{}", self.server()?, self.port()).to_socket_addrs() + format!("{}:{}", self.server()?, self.port()) + .to_socket_addrs() .map(|mut i| i.next().unwrap()) .map_err(|e| e.into()) } @@ -442,17 +428,16 @@ 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. @@ -471,16 +456,15 @@ impl Config { /// This defaults to `irc:version:env` when not specified. /// For example, `irc:0.12.0:Compiled with rustc` pub fn version(&self) -> &str { - self.version.as_ref().map_or(::VERSION_STR, |s| &s) + self.version.as_ref().map_or(crate::VERSION_STR, |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[..], - ) + self.source + .as_ref() + .map_or("https://github.com/aatxe/irc", |s| &s[..]) } /// Gets the amount of time in seconds for the interval at which the client pings the server. @@ -522,16 +506,16 @@ 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. pub fn get_option(&self, option: &str) -> Option<&str> { - self.options.as_ref().and_then(|o| { - o.get(&option.to_owned()).map(|s| &s[..]) - }) + self.options + .as_ref() + .and_then(|o| o.get(&option.to_owned()).map(|s| &s[..])) } /// Gets whether or not to use a mock connection for testing. @@ -550,12 +534,8 @@ impl Config { #[cfg(test)] mod test { - use std::collections::HashMap; - use std::default::Default; - #[cfg(feature = "json")] - use std::path::Path; - use super::Config; + use std::collections::HashMap; #[allow(unused)] fn test_config() -> Config { @@ -594,24 +574,6 @@ mod test { } } - #[test] - #[cfg(feature = "json")] - fn load_from_json() { - assert_eq!(Config::load("client_config.json").unwrap(), test_config().with_path("client_config.json")); - } - - #[test] - #[cfg(feature = "toml")] - fn load_from_toml() { - assert_eq!(Config::load("client_config.toml").unwrap(), test_config().with_path("client_config.toml")); - } - - #[test] - #[cfg(feature = "yaml")] - fn load_from_yaml() { - assert_eq!(Config::load("client_config.yaml").unwrap(), test_config().with_path("client_config.yaml")); - } - #[test] fn is_owner() { let cfg = Config { @@ -636,4 +598,37 @@ mod test { assert_eq!(cfg.get_option("testing"), Some("test")); assert_eq!(cfg.get_option("not"), None); } + + #[test] + #[cfg(feature = "json")] + fn load_from_json() -> Result<(), failure::Error> { + const DATA: &str = include_str!("client_config.json"); + assert_eq!( + Config::load_json("client_config.json", DATA)?.with_path("client_config.json"), + test_config().with_path("client_config.json") + ); + Ok(()) + } + + #[test] + #[cfg(feature = "toml")] + fn load_from_toml() -> Result<(), failure::Error> { + const DATA: &str = include_str!("client_config.toml"); + assert_eq!( + Config::load_toml("client_config.toml", DATA)?.with_path("client_config.toml"), + test_config().with_path("client_config.toml") + ); + Ok(()) + } + + #[test] + #[cfg(feature = "yaml")] + fn load_from_yaml() -> Result<(), failure::Error> { + const DATA: &str = include_str!("client_config.yaml"); + assert_eq!( + Config::load_yaml("client_config.yaml", DATA)?.with_path("client_config.yaml"), + test_config().with_path("client_config.yaml") + ); + Ok(()) + } } diff --git a/src/client/data/mod.rs b/src/client/data/mod.rs index 8484550..e9f4849 100644 --- a/src/client/data/mod.rs +++ b/src/client/data/mod.rs @@ -1,7 +1,7 @@ //! Data related to IRC functionality. -pub use client::data::config::Config; -pub use client::data::user::{AccessLevel, User}; +pub use crate::client::data::config::Config; +pub use crate::client::data::user::{AccessLevel, User}; pub mod config; pub mod user; diff --git a/src/client/data/user.rs b/src/client/data/user.rs index eabf3a9..d37a792 100644 --- a/src/client/data/user.rs +++ b/src/client/data/user.rs @@ -1,10 +1,10 @@ //! Data for tracking user information. use std::borrow::ToOwned; use std::cmp::Ordering; -use std::cmp::Ordering::{Less, Equal, Greater}; +use std::cmp::Ordering::{Equal, Greater, Less}; use std::str::FromStr; -use proto::{Mode, ChannelMode}; +use crate::proto::{ChannelMode, Mode}; /// IRC User data. #[derive(Clone, Debug)] @@ -124,8 +124,9 @@ 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.nickname == other.nickname + && self.username == other.username + && self.hostname == other.hostname } } @@ -208,7 +209,9 @@ struct AccessLevelIterator { impl AccessLevelIterator { pub fn new(value: &str) -> AccessLevelIterator { - AccessLevelIterator { value: value.to_owned() } + AccessLevelIterator { + value: value.to_owned(), + } } } @@ -225,10 +228,10 @@ impl Iterator for AccessLevelIterator { #[cfg(test)] mod test { - use super::{AccessLevel, User}; use super::AccessLevel::*; - use proto::ChannelMode as M; - use proto::Mode::*; + use super::{AccessLevel, User}; + use crate::proto::ChannelMode as M; + use crate::proto::Mode::*; #[test] fn parse_access_level() { @@ -269,7 +272,6 @@ mod test { assert_eq!(user, exp); assert_eq!(user.highest_access_level, exp.highest_access_level); assert_eq!(user.access_levels, exp.access_levels); - } #[test] diff --git a/src/client/ext.rs b/src/client/ext.rs deleted file mode 100644 index f82c53d..0000000 --- a/src/client/ext.rs +++ /dev/null @@ -1,687 +0,0 @@ -//! Utilities and shortcuts for working with IRC servers. -//! -//! This module provides the [`ClientExt`](trait.ClientExt.html) trait which is the idiomatic way of -//! sending messages to an IRC server. This trait is automatically implemented for everything that -//! implements [`Client`](../trait.Client.html) and is designed to provide important functionality -//! without clutter. -//! -//! # Examples -//! -//! Using these APIs, we can connect to a server and send a one-off message (in this case, -//! identifying with the server). -//! -//! ```no_run -//! # extern crate irc; -//! use irc::client::prelude::{IrcClient, ClientExt}; -//! -//! # fn main() { -//! let server = IrcClient::new("config.toml").unwrap(); -//! // identify and send_privmsg both come from `ClientExt` -//! server.identify().unwrap(); -//! server.send_privmsg("#example", "Hello, world!").unwrap(); -//! # } -//! ``` -//! -//! `ClientExt::identify` also plays an important role in performing IRCv3 capability negotiations. -//! In particular, calling `identify` will close the negotiations (and otherwise indicate IRCv3 -//! compatibility). This means that all IRCv3 capability requests should be performed before calling -//! `identify`. For example: -//! -//! ```no_run -//! # extern crate irc; -//! # use irc::client::prelude::*; -//! # fn main() { -//! # let server = IrcClient::new("config.toml").unwrap(); -//! server.send_cap_req(&[Capability::MultiPrefix, Capability::UserhostInNames]).unwrap(); -//! server.identify().unwrap(); -//! # } -//! ``` -use std::string::ToString; - -#[cfg(feature = "ctcp")] -use chrono::prelude::*; - -use error::Result; -use proto::{Capability, Command, Mode, NegotiationVersion}; -use proto::command::CapSubCommand::{END, LS, REQ}; -use proto::command::Command::*; -use proto::mode::ModeType; -use client::Client; - -/// Idiomatic extensions for sending messages to an IRC server as a [`Client`](../trait.Client.html). -pub trait ClientExt: Client { - /// 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(Command::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, - ); - 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, - { - // Send a CAP END to signify that we're IRCv3-compliant (and to end negotiations!). - self.send(CAP(None, END, None, None))?; - if self.config().password() != "" { - self.send(PASS(self.config().password().to_owned()))?; - } - self.send(NICK(self.config().nickname()?.to_owned()))?; - 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: S) -> Result<()> - where - Self: Sized, - { - self.send(AUTHENTICATE(data.to_string())) - } - - /// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism. - 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, - { - self.send_sasl("EXTERNAL") - } - - /// Sends a SASL AUTHENTICATE request to abort authentication. - fn send_sasl_abort(&self) -> Result<()> - where - Self: Sized, - { - self.send_sasl("*") - } - - /// Sends a PONG with the specified message. - fn send_pong(&self, msg: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - self.send(PONG(msg.to_string(), None)) - } - - /// Joins the specified channel or chanlist. - fn send_join(&self, chanlist: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - self.send(JOIN(chanlist.to_string(), 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, - S1: ToString, - S2: ToString, - { - self.send(JOIN(chanlist.to_string(), Some(keylist.to_string()), None)) - } - - /// Parts the specified channel or chanlist. - fn send_part(&self, chanlist: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - self.send(PART(chanlist.to_string(), None)) - } - - /// Attempts to oper up using the specified username and password. - fn send_oper(&self, username: S1, password: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - self.send(OPER(username.to_string(), password.to_string())) - } - - /// Sends a message to the specified target. If the message contains IRC newlines (`\r\n`), it - /// will automatically be split and sent as multiple separate `PRIVMSG`s to the specified - /// target. If you absolutely must avoid this behavior, you can do - /// `client.send(PRIVMSG(target, message))` directly. - fn send_privmsg(&self, target: S1, message: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - let message = message.to_string(); - for line in message.split("\r\n") { - self.send(PRIVMSG(target.to_string(), line.to_string()))? - } - Ok(()) - } - - /// Sends a notice to the specified target. - fn send_notice(&self, target: S1, message: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - let message = message.to_string(); - for line in message.split("\r\n") { - self.send(NOTICE(target.to_string(), line.to_string()))? - } - Ok(()) - } - - /// 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: S1, topic: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - let topic = topic.to_string(); - self.send(TOPIC( - channel.to_string(), - if topic.is_empty() { - None - } else { - Some(topic) - }, - )) - } - - /// Kills the target with the provided message. - fn send_kill(&self, target: S1, message: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - self.send(KILL(target.to_string(), message.to_string())) - } - - /// 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: S1, nicklist: S2, message: S3) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - S3: ToString, - { - let message = message.to_string(); - self.send(KICK( - chanlist.to_string(), - nicklist.to_string(), - if message.is_empty() { - None - } else { - Some(message) - }, - )) - } - - /// Changes the modes for the specified target. - fn send_mode(&self, target: S, modes: &[Mode]) -> Result<()> - where - Self: Sized, - S: ToString, - T: ModeType, - { - self.send(T::mode(&target.to_string(), modes)) - } - - /// 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: S1, mode: S2, modeparams: S3) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - S3: ToString, - { - let modeparams = modeparams.to_string(); - self.send(SAMODE( - target.to_string(), - mode.to_string(), - if modeparams.is_empty() { - None - } else { - Some(modeparams) - }, - )) - } - - /// Forces a user to change from the old nickname to the new nickname. - fn send_sanick(&self, old_nick: S1, new_nick: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - self.send(SANICK(old_nick.to_string(), new_nick.to_string())) - } - - /// Invites a user to the specified channel. - fn send_invite(&self, nick: S1, chan: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - self.send(INVITE(nick.to_string(), chan.to_string())) - } - - /// Quits the server entirely with a message. - /// This defaults to `Powered by Rust.` if none is specified. - fn send_quit(&self, msg: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - let msg = msg.to_string(); - self.send(QUIT(Some(if msg.is_empty() { - "Powered by Rust.".to_string() - } else { - msg - }))) - } - - /// 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: S1, msg: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - self.send_privmsg(target, &format!("\u{001}{}\u{001}", msg.to_string())[..]) - } - - /// Sends an action command to the specified target. - /// This requires the CTCP feature to be enabled. - #[cfg(feature = "ctcp")] - fn send_action(&self, target: S1, msg: S2) -> Result<()> - where - Self: Sized, - S1: ToString, - S2: ToString, - { - self.send_ctcp(target, &format!("ACTION {}", msg.to_string())[..]) - } - - /// Sends a finger request to the specified target. - /// This requires the CTCP feature to be enabled. - #[cfg(feature = "ctcp")] - fn send_finger(&self, target: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - 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: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - 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: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - 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: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - 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: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - let time = Local::now(); - self.send_ctcp(target, &format!("PING {}", time.timestamp())[..]) - } - - /// Sends a time request to the specified target. - /// This requires the CTCP feature to be enabled. - #[cfg(feature = "ctcp")] - fn send_time(&self, target: S) -> Result<()> - where - Self: Sized, - S: ToString, - { - self.send_ctcp(target, "TIME") - } -} - -impl ClientExt for C where C: Client {} - -#[cfg(test)] -mod test { - use super::ClientExt; - use client::data::Config; - use client::IrcClient; - use client::test::{get_client_value, test_config}; - use proto::{ChannelMode, Mode}; - - #[test] - fn identify() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.identify().unwrap(); - assert_eq!( - &get_client_value(client)[..], - "CAP END\r\nNICK :test\r\n\ - USER test 0 * :test\r\n" - ); - } - - #[test] - fn identify_with_password() { - let client = IrcClient::from_config(Config { - nickname: Some(format!("test")), - password: Some(format!("password")), - ..test_config() - }).unwrap(); - client.identify().unwrap(); - assert_eq!( - &get_client_value(client)[..], - "CAP END\r\nPASS :password\r\nNICK :test\r\n\ - USER test 0 * :test\r\n" - ); - } - - #[test] - fn send_pong() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_pong("irc.test.net").unwrap(); - assert_eq!(&get_client_value(client)[..], "PONG :irc.test.net\r\n"); - } - - #[test] - fn send_join() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_join("#test,#test2,#test3").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "JOIN #test,#test2,#test3\r\n" - ); - } - - #[test] - fn send_part() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_part("#test").unwrap(); - assert_eq!(&get_client_value(client)[..], "PART #test\r\n"); - } - - #[test] - fn send_oper() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_oper("test", "test").unwrap(); - assert_eq!(&get_client_value(client)[..], "OPER test :test\r\n"); - } - - #[test] - fn send_privmsg() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_privmsg("#test", "Hi, everybody!").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "PRIVMSG #test :Hi, everybody!\r\n" - ); - } - - #[test] - fn send_notice() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_notice("#test", "Hi, everybody!").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "NOTICE #test :Hi, everybody!\r\n" - ); - } - - #[test] - fn send_topic_no_topic() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_topic("#test", "").unwrap(); - assert_eq!(&get_client_value(client)[..], "TOPIC #test\r\n"); - } - - #[test] - fn send_topic() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_topic("#test", "Testing stuff.").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "TOPIC #test :Testing stuff.\r\n" - ); - } - - #[test] - fn send_kill() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_kill("test", "Testing kills.").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "KILL test :Testing kills.\r\n" - ); - } - - #[test] - fn send_kick_no_message() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_kick("#test", "test", "").unwrap(); - assert_eq!(&get_client_value(client)[..], "KICK #test test\r\n"); - } - - #[test] - fn send_kick() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_kick("#test", "test", "Testing kicks.").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "KICK #test test :Testing kicks.\r\n" - ); - } - - #[test] - fn send_mode_no_modeparams() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap(); - assert_eq!(&get_client_value(client)[..], "MODE #test +i\r\n"); - } - - #[test] - fn send_mode() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))]) - .unwrap(); - assert_eq!(&get_client_value(client)[..], "MODE #test +o test\r\n"); - } - - #[test] - fn send_samode_no_modeparams() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_samode("#test", "+i", "").unwrap(); - assert_eq!(&get_client_value(client)[..], "SAMODE #test +i\r\n"); - } - - #[test] - fn send_samode() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_samode("#test", "+o", "test").unwrap(); - assert_eq!(&get_client_value(client)[..], "SAMODE #test +o test\r\n"); - } - - #[test] - fn send_sanick() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_sanick("test", "test2").unwrap(); - assert_eq!(&get_client_value(client)[..], "SANICK test test2\r\n"); - } - - #[test] - fn send_invite() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_invite("test", "#test").unwrap(); - assert_eq!(&get_client_value(client)[..], "INVITE test #test\r\n"); - } - - #[test] - #[cfg(feature = "ctcp")] - fn send_ctcp() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_ctcp("test", "MESSAGE").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "PRIVMSG test :\u{001}MESSAGE\u{001}\r\n" - ); - } - - #[test] - #[cfg(feature = "ctcp")] - fn send_action() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_action("test", "tests.").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n" - ); - } - - #[test] - #[cfg(feature = "ctcp")] - fn send_finger() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_finger("test").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "PRIVMSG test :\u{001}FINGER\u{001}\r\n" - ); - } - - #[test] - #[cfg(feature = "ctcp")] - fn send_version() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_version("test").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "PRIVMSG test :\u{001}VERSION\u{001}\r\n" - ); - } - - #[test] - #[cfg(feature = "ctcp")] - fn send_source() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_source("test").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "PRIVMSG test :\u{001}SOURCE\u{001}\r\n" - ); - } - - #[test] - #[cfg(feature = "ctcp")] - fn send_user_info() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_user_info("test").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "PRIVMSG test :\u{001}USERINFO\u{001}\r\n" - ); - } - - #[test] - #[cfg(feature = "ctcp")] - fn send_ctcp_ping() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_ctcp_ping("test").unwrap(); - let val = get_client_value(client); - println!("{}", val); - assert!(val.starts_with("PRIVMSG test :\u{001}PING ")); - assert!(val.ends_with("\u{001}\r\n")); - } - - #[test] - #[cfg(feature = "ctcp")] - fn send_time() { - let client = IrcClient::from_config(test_config()).unwrap(); - client.send_time("test").unwrap(); - assert_eq!( - &get_client_value(client)[..], - "PRIVMSG test :\u{001}TIME\u{001}\r\n" - ); - } -} diff --git a/src/client/mock.rs b/src/client/mock.rs new file mode 100644 index 0000000..e1c3334 --- /dev/null +++ b/src/client/mock.rs @@ -0,0 +1,66 @@ +use std::{ + io::{self, Cursor, Read, Write}, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncWrite}; + +/// A fake stream for testing network applications backed by buffers. +#[derive(Clone, Debug)] +pub struct MockStream { + written: Cursor>, + received: Cursor>, +} + +impl MockStream { + /// Creates a new mock stream with nothing to read. + pub fn empty() -> MockStream { + MockStream::new(&[]) + } + + /// Creates a new mock stream with the specified bytes to read. + pub fn new(initial: &[u8]) -> MockStream { + MockStream { + written: Cursor::new(vec![]), + received: Cursor::new(initial.to_owned()), + } + } + + /// Gets a slice of bytes representing the data that has been written. + pub fn written(&self) -> &[u8] { + self.written.get_ref() + } + + /// Gets a slice of bytes representing the data that has been received. + pub fn received(&self) -> &[u8] { + self.received.get_ref() + } +} + +impl AsyncRead for MockStream { + fn poll_read( + mut self: Pin<&mut Self>, + _: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + Poll::Ready(self.as_mut().received.read(buf)) + } +} + +impl AsyncWrite for MockStream { + fn poll_write( + mut self: Pin<&mut Self>, + _: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready(self.as_mut().written.write(buf)) + } + + fn poll_flush(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(self.as_mut().written.flush()) + } + + fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 1588898..f945550 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,11 +1,9 @@ //! A simple, thread-safe, and async-friendly IRC client library. //! //! This API provides the ability to connect to an IRC server via the -//! [`IrcClient`](struct.IrcClient.html) type. The [`Client`](trait.Client.html) trait that -//! [`IrcClient`](struct.IrcClient.html) implements provides methods for communicating with the -//! server. An extension trait, [`ClientExt`](./ext/trait.ClientExt.html), provides short-hand for -//! sending a variety of important messages without referring to their entries in -//! [`proto::command`](../proto/command/enum.Command.html). +//! [`Client`](struct.Client.html) type. The [`Client`](trait.Client.html) trait that +//! [`Client`](struct.Client.html) implements provides methods for communicating with the +//! server. //! //! # Examples //! @@ -14,12 +12,13 @@ //! //! ```no_run //! # extern crate irc; -//! use irc::client::prelude::{IrcClient, ClientExt}; +//! use irc::client::prelude::Client; //! -//! # fn main() { -//! let client = IrcClient::new("config.toml").unwrap(); -//! // identify comes from `ClientExt` -//! client.identify().unwrap(); +//! # #[tokio::main] +//! # async fn main() -> irc::error::Result<()> { +//! let client = Client::new("config.toml").await?; +//! client.identify()?; +//! # Ok(()) //! # } //! ``` //! @@ -28,172 +27,393 @@ //! performs a simple call-and-response when the bot's name is mentioned in a channel. //! //! ```no_run -//! # extern crate irc; -//! # use irc::client::prelude::{IrcClient, ClientExt}; -//! use irc::client::prelude::{Client, Command}; +//! use irc::client::prelude::*; +//! use futures::*; //! -//! # fn main() { -//! # let client = IrcClient::new("config.toml").unwrap(); -//! # client.identify().unwrap(); -//! client.for_each_incoming(|irc_msg| { -//! if let Command::PRIVMSG(channel, message) = irc_msg.command { +//! # #[tokio::main] +//! # async fn main() -> irc::error::Result<()> { +//! let mut client = Client::new("config.toml").await?; +//! let mut stream = client.stream()?; +//! client.identify()?; +//! +//! while let Some(message) = stream.next().await.transpose()? { +//! if let Command::PRIVMSG(channel, message) = message.command { //! if message.contains(client.current_nickname()) { //! client.send_privmsg(&channel, "beep boop").unwrap(); //! } //! } -//! }).unwrap(); +//! } +//! # Ok(()) //! # } //! ``` -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::runtime::current_thread::Runtime; +use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use futures_util::{ + future::{FusedFuture, Future}, + ready, + stream::{FusedStream, Stream}, +}; +use futures_util::{ + sink::Sink as _, + stream::{SplitSink, SplitStream, StreamExt as _}, +}; +use parking_lot::RwLock; +use std::{ + collections::HashMap, + fmt, + path::Path, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; -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}; +use crate::{ + client::{ + conn::Connection, + data::{Config, User}, + }, + error, + proto::{ + mode::ModeType, + CapSubCommand::{END, LS, REQ}, + Capability, ChannelMode, Command, + Command::{ + ChannelMODE, AUTHENTICATE, CAP, INVITE, JOIN, KICK, KILL, NICK, NICKSERV, NOTICE, OPER, + PART, PASS, PONG, PRIVMSG, QUIT, SAMODE, SANICK, TOPIC, USER, + }, + Message, Mode, NegotiationVersion, Response, + }, +}; pub mod conn; pub mod data; -pub mod ext; +mod mock; pub mod prelude; -pub mod reactor; pub mod transport; -/// Trait extending all IRC streams with `for_each_incoming` convenience function. -/// -/// This is typically used in conjunction with [`Client::stream`](trait.Client.html#tymethod.stream) -/// in order to use an API akin to -/// [`Client::for_each_incoming`](trait.Client.html#method.for_each_incoming). -/// -/// # Example -/// -/// ```no_run -/// # extern crate irc; -/// # use irc::client::prelude::{IrcClient, Client, Command, ClientExt}; -/// use irc::client::prelude::EachIncomingExt; -/// -/// # fn main() { -/// # let client = IrcClient::new("config.toml").unwrap(); -/// # client.identify().unwrap(); -/// client.stream().for_each_incoming(|irc_msg| { -/// match irc_msg.command { -/// Command::PRIVMSG(channel, message) => if message.contains(client.current_nickname()) { -/// client.send_privmsg(&channel, "beep boop").unwrap(); -/// } -/// _ => () -/// } -/// }).unwrap(); -/// # } -/// ``` -pub trait EachIncomingExt: Stream { - /// Blocks on the stream, running the given function on each incoming message as they arrive. - fn for_each_incoming(self, mut f: F) -> error::Result<()> - where F: FnMut(Message) -> (), Self: Sized { - self.for_each(|msg| { - f(msg); - Ok(()) - }).wait() +macro_rules! pub_state_base { + () => { + /// Changes the modes for the specified target. + pub fn send_mode(&self, target: S, modes: &[Mode]) -> error::Result<()> + where + S: fmt::Display, + T: ModeType, + { + self.send(T::mode(&target.to_string(), modes)) + } + + /// Joins the specified channel or chanlist. + pub fn send_join(&self, chanlist: S) -> error::Result<()> + where + S: fmt::Display, + { + self.send(JOIN(chanlist.to_string(), None, None)) + } + + /// Joins the specified channel or chanlist using the specified key or keylist. + pub fn send_join_with_keys(&self, chanlist: &str, keylist: &str) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + self.send(JOIN(chanlist.to_string(), Some(keylist.to_string()), None)) + } + + /// Sends a notice to the specified target. + pub fn send_notice(&self, target: S1, message: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + let message = message.to_string(); + for line in message.split("\r\n") { + self.send(NOTICE(target.to_string(), line.to_string()))? + } + Ok(()) + } } } -impl EachIncomingExt for T where T: Stream {} - -/// An interface for communicating with an IRC server. -pub trait Client { - /// Gets the configuration being used with this `Client`. - fn config(&self) -> &Config; - - /// Sends a [`Command`](../proto/command/enum.Command.html) as this `Client`. This is the - /// core primitive for sending messages to the server. In practice, it's often more pleasant - /// (and more idiomatic) to use the functions defined on - /// [`ClientExt`](./ext/trait.ClientExt.html). They capture a lot of the more repetitive - /// aspects of sending messages. - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use irc::client::prelude::*; - /// # fn main() { - /// # let client = IrcClient::new("config.toml").unwrap(); - /// client.send(Command::NICK("example".to_owned())).unwrap(); - /// client.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap(); - /// # } - /// ``` - fn send>(&self, message: M) -> error::Result<()> where Self: Sized; - - /// Gets a stream of incoming messages from the `Client`'s connection. This is only necessary - /// when trying to set up more complex clients, and requires use of the `futures` crate. Most - /// IRC bots should be able to get by using only `for_each_incoming` to handle received - /// messages. You can find some examples of more complex setups using `stream` in the - /// [GitHub repository](https://github.com/aatxe/irc/tree/stable/examples). - /// - /// **Note**: The stream can only be returned once. Subsequent attempts will cause a panic. - // FIXME: when impl traits stabilize, we should change this return type. - fn stream(&self) -> ClientStream; - - /// Blocks on the stream, running the given function on each incoming message as they arrive. - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use irc::client::prelude::{IrcClient, ClientExt, Client, Command}; - /// # fn main() { - /// # let client = IrcClient::new("config.toml").unwrap(); - /// # client.identify().unwrap(); - /// client.for_each_incoming(|irc_msg| { - /// if let Command::PRIVMSG(channel, message) = irc_msg.command { - /// if message.contains(client.current_nickname()) { - /// client.send_privmsg(&channel, "beep boop").unwrap(); - /// } - /// } - /// }).unwrap(); - /// # } - /// ``` - fn for_each_incoming(&self, f: F) -> error::Result<()> where F: FnMut(Message) -> () { - self.stream().for_each_incoming(f) +macro_rules! pub_sender_base { + () => { + /// Sends a request for a list of server capabilities for a specific IRCv3 version. + pub fn send_cap_ls(&self, version: NegotiationVersion) -> error::Result<()> { + self.send(Command::CAP( + None, + LS, + match version { + NegotiationVersion::V301 => None, + NegotiationVersion::V302 => Some("302".to_owned()), + }, + None, + )) } - /// Gets a list of currently joined channels. This will be `None` if tracking is disabled - /// altogether via the `nochanlists` feature. - fn list_channels(&self) -> Option>; + /// Sends an IRCv3 capabilities request for the specified extensions. + pub fn send_cap_req(&self, extensions: &[Capability]) -> error::Result<()> { + 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))) + } - /// Gets a list of [`Users`](./data/user/struct.User.html) in the specified channel. If the - /// specified channel hasn't been joined or the `nochanlists` feature is enabled, this function - /// will return `None`. - /// - /// For best results, be sure to request `multi-prefix` support from the server. This will allow - /// for more accurate tracking of user rank (e.g. oper, half-op, etc.). - /// # Requesting multi-prefix support - /// ```no_run - /// # extern crate irc; - /// # use irc::client::prelude::{IrcClient, ClientExt, Client, Command}; - /// use irc::proto::caps::Capability; - /// - /// # fn main() { - /// # let client = IrcClient::new("config.toml").unwrap(); - /// client.send_cap_req(&[Capability::MultiPrefix]).unwrap(); - /// client.identify().unwrap(); - /// # } - /// ``` - fn list_users(&self, channel: &str) -> Option>; + /// Sends a SASL AUTHENTICATE message with the specified data. + pub fn send_sasl(&self, data: S) -> error::Result<()> { + self.send(AUTHENTICATE(data.to_string())) + } + + /// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism. + pub fn send_sasl_plain(&self) -> error::Result<()> { + self.send_sasl("PLAIN") + } + + /// Sends a SASL AUTHENTICATE request to use the EXTERNAL mechanism. + pub fn send_sasl_external(&self) -> error::Result<()> { + self.send_sasl("EXTERNAL") + } + + /// Sends a SASL AUTHENTICATE request to abort authentication. + pub fn send_sasl_abort(&self) -> error::Result<()> { + self.send_sasl("*") + } + + /// Sends a PONG with the specified message. + pub fn send_pong(&self, msg: S) -> error::Result<()> + where + S: fmt::Display, + { + self.send(PONG(msg.to_string(), None)) + } + + /// Parts the specified channel or chanlist. + pub fn send_part(&self, chanlist: S) -> error::Result<()> + where + S: fmt::Display, + { + self.send(PART(chanlist.to_string(), None)) + } + + /// Attempts to oper up using the specified username and password. + pub fn send_oper(&self, username: S1, password: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + self.send(OPER(username.to_string(), password.to_string())) + } + + /// Sends a message to the specified target. If the message contains IRC newlines (`\r\n`), it + /// will automatically be split and sent as multiple separate `PRIVMSG`s to the specified + /// target. If you absolutely must avoid this behavior, you can do + /// `client.send(PRIVMSG(target, message))` directly. + pub fn send_privmsg(&self, target: S1, message: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + let message = message.to_string(); + for line in message.split("\r\n") { + self.send(PRIVMSG(target.to_string(), line.to_string()))? + } + Ok(()) + } + + /// 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. + pub fn send_topic(&self, channel: S1, topic: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + let topic = topic.to_string(); + self.send(TOPIC( + channel.to_string(), + if topic.is_empty() { None } else { Some(topic) }, + )) + } + + /// Kills the target with the provided message. + pub fn send_kill(&self, target: S1, message: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + self.send(KILL(target.to_string(), message.to_string())) + } + + /// 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. + pub fn send_kick( + &self, + chanlist: S1, + nicklist: S2, + message: S3, + ) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + S3: fmt::Display, + { + let message = message.to_string(); + self.send(KICK( + chanlist.to_string(), + nicklist.to_string(), + if message.is_empty() { + None + } else { + Some(message) + }, + )) + } + + /// Changes the mode of the target by force. + /// If `modeparams` is an empty string, it won't be included in the message. + pub fn send_samode(&self, target: S1, mode: S2, modeparams: S3) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + S3: fmt::Display, + { + let modeparams = modeparams.to_string(); + self.send(SAMODE( + target.to_string(), + mode.to_string(), + if modeparams.is_empty() { + None + } else { + Some(modeparams) + }, + )) + } + + /// Forces a user to change from the old nickname to the new nickname. + pub fn send_sanick(&self, old_nick: S1, new_nick: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + self.send(SANICK(old_nick.to_string(), new_nick.to_string())) + } + + /// Invites a user to the specified channel. + pub fn send_invite(&self, nick: S1, chan: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + self.send(INVITE(nick.to_string(), chan.to_string())) + } + + /// Quits the server entirely with a message. + /// This defaults to `Powered by Rust.` if none is specified. + pub fn send_quit(&self, msg: S) -> error::Result<()> + where + S: fmt::Display, + { + let msg = msg.to_string(); + self.send(QUIT(Some(if msg.is_empty() { + "Powered by Rust.".to_string() + } else { + msg + }))) + } + + /// Sends a CTCP-escaped message to the specified target. + /// This requires the CTCP feature to be enabled. + #[cfg(feature = "ctcp")] + pub fn send_ctcp(&self, target: S1, msg: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + self.send_privmsg(target, &format!("\u{001}{}\u{001}", msg.to_string())[..]) + } + + /// Sends an action command to the specified target. + /// This requires the CTCP feature to be enabled. + #[cfg(feature = "ctcp")] + pub fn send_action(&self, target: S1, msg: S2) -> error::Result<()> + where + S1: fmt::Display, + S2: fmt::Display, + { + self.send_ctcp(target, &format!("ACTION {}", msg.to_string())[..]) + } + + /// Sends a finger request to the specified target. + /// This requires the CTCP feature to be enabled. + #[cfg(feature = "ctcp")] + pub fn send_finger(&self, target: S) -> error::Result<()> + where + S: fmt::Display, + { + self.send_ctcp(target, "FINGER") + } + + /// Sends a version request to the specified target. + /// This requires the CTCP feature to be enabled. + #[cfg(feature = "ctcp")] + pub fn send_version(&self, target: S) -> error::Result<()> + where + S: fmt::Display, + { + self.send_ctcp(target, "VERSION") + } + + /// Sends a source request to the specified target. + /// This requires the CTCP feature to be enabled. + #[cfg(feature = "ctcp")] + pub fn send_source(&self, target: S) -> error::Result<()> + where + S: fmt::Display, + { + 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")] + pub fn send_user_info(&self, target: S) -> error::Result<()> + where + S: fmt::Display, + { + self.send_ctcp(target, "USERINFO") + } + + /// Sends a finger request to the specified target. + /// This requires the CTCP feature to be enabled. + #[cfg(feature = "ctcp")] + pub fn send_ctcp_ping(&self, target: S) -> error::Result<()> + where + S: fmt::Display, + { + let time = Local::now(); + self.send_ctcp(target, &format!("PING {}", time.timestamp())[..]) + } + + /// Sends a time request to the specified target. + /// This requires the CTCP feature to be enabled. + #[cfg(feature = "ctcp")] + pub fn send_time(&self, target: S) -> error::Result<()> + where + S: fmt::Display, + { + self.send_ctcp(target, "TIME") + } + } } -/// A stream of `Messages` received from an IRC server via an `IrcClient`. +/// A stream of `Messages` received from an IRC server via an `Client`. /// /// Interaction with this stream relies on the `futures` API, but is only expected for less /// traditional use cases. To learn more, you can view the documentation for the @@ -203,19 +423,57 @@ pub trait Client { pub struct ClientStream { state: Arc, stream: SplitStream, + // In case the client stream also handles outgoing messages. + outgoing: Option, +} + +impl ClientStream { + /// collect stream and collect all messages available. + pub async fn collect(mut self) -> error::Result> { + let mut output = Vec::new(); + + while let Some(message) = self.next().await { + match message { + Ok(message) => output.push(message), + Err(e) => return Err(e), + } + } + + Ok(output) + } +} + +impl FusedStream for ClientStream { + fn is_terminated(&self) -> bool { + false + } } impl Stream for ClientStream { - type Item = Message; - type Error = error::IrcError; + type Item = Result; - fn poll(&mut self) -> Poll, Self::Error> { - match try_ready!(self.stream.poll()) { - Some(msg) => { - self.state.handle_message(&msg)?; - Ok(Async::Ready(Some(msg))) + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if let Some(outgoing) = self.as_mut().outgoing.as_mut() { + match Pin::new(outgoing).poll(cx) { + Poll::Ready(Ok(())) => { + // assure that we wake up again to check the incoming stream. + cx.waker().wake_by_ref(); + return Poll::Ready(None); + } + Poll::Ready(Err(e)) => { + cx.waker().wake_by_ref(); + return Poll::Ready(Some(Err(e))); + } + Poll::Pending => (), } - None => Ok(Async::Ready(None)), + } + + match ready!(Pin::new(&mut self.as_mut().stream).poll_next(cx)) { + Some(Ok(msg)) => { + self.state.handle_message(&msg)?; + return Poll::Ready(Some(Ok(msg))); + } + other => Poll::Ready(other), } } } @@ -223,107 +481,66 @@ impl Stream for ClientStream { /// Thread-safe internal state for an IRC server connection. #[derive(Debug)] struct ClientState { + sender: Sender, /// The configuration used with this connection. config: Config, /// A thread-safe map of channels to the list of users in them. - chanlists: Mutex>>, + chanlists: RwLock>>, /// A thread-safe index to track the current alternative nickname being used. alt_nick_index: RwLock, - /// A thread-safe internal IRC stream used for the reading API. - incoming: Mutex>>, - /// A thread-safe copy of the outgoing channel. - outgoing: UnboundedSender, } -impl<'a> Client for ClientState { +impl ClientState { + fn new(sender: Sender, config: Config) -> ClientState { + ClientState { + sender, + config: config, + chanlists: RwLock::new(HashMap::new()), + alt_nick_index: RwLock::new(0), + } + } + fn config(&self) -> &Config { &self.config } - fn send>(&self, msg: M) -> error::Result<()> where Self: Sized { + fn send>(&self, msg: M) -> error::Result<()> { 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> { - Some( - self.chanlists - .lock() - .unwrap() - .keys() - .map(|k| k.to_owned()) - .collect(), - ) - } - - #[cfg(feature = "nochanlists")] - fn list_channels(&self) -> Option> { - None - } - - #[cfg(not(feature = "nochanlists"))] - fn list_users(&self, chan: &str) -> Option> { - self.chanlists - .lock() - .unwrap() - .get(&chan.to_owned()) - .cloned() - } - - #[cfg(feature = "nochanlists")] - fn list_users(&self, _: &str) -> Option> { - None - } -} - -impl ClientState { - fn new( - incoming: SplitStream, - outgoing: UnboundedSender, - config: Config, - ) -> ClientState { - ClientState { - config: config, - chanlists: Mutex::new(HashMap::new()), - alt_nick_index: RwLock::new(0), - incoming: Mutex::new(Some(incoming)), - outgoing: outgoing, - } + Ok(self.sender.send(msg)?) } /// 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(); + let index = self.alt_nick_index.read(); + match *index { - 0 => self.config().nickname().expect( - "current_nickname should not be callable if nickname is not defined." - ), + 0 => self + .config() + .nickname() + .expect("current_nickname should not be callable if nickname is not defined."), i => alt_nicks[i - 1], } } /// Handles sent messages internally for basic client functionality. fn handle_sent_message(&self, msg: &Message) -> error::Result<()> { - trace!("[SENT] {}", msg.to_string()); + log::trace!("[SENT] {}", msg.to_string()); + match msg.command { PART(ref chan, _) => { - let _ = self.chanlists.lock().unwrap().remove(chan); + let _ = self.chanlists.write().remove(chan); } _ => (), } + Ok(()) } /// Handles received messages internally for basic client functionality. fn handle_message(&self, msg: &Message) -> error::Result<()> { - trace!("[RECV] {}", msg.to_string()); + log::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), @@ -353,8 +570,8 @@ impl ClientState { Command::Response(Response::RPL_NAMREPLY, ref args, ref suffix) => { self.handle_namreply(args, suffix) } - Command::Response(Response::RPL_ENDOFMOTD, _, _) | - Command::Response(Response::ERR_NOMOTD, _, _) => { + Command::Response(Response::RPL_ENDOFMOTD, _, _) + | Command::Response(Response::ERR_NOMOTD, _, _) => { self.send_nick_password()?; self.send_umodes()?; @@ -365,20 +582,21 @@ impl ClientState { 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()), - ) + let joined_chans = self.chanlists.read(); + 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, _, _) => { + 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(); + let mut index = self.alt_nick_index.write(); + if *index >= alt_nicks.len() { - return Err(error::IrcError::NoUsableNick); + return Err(error::Error::NoUsableNick); } else { self.send(NICK(alt_nicks[*index].to_owned()))?; *index += 1; @@ -393,7 +611,8 @@ impl ClientState { if self.config().nick_password().is_empty() { Ok(()) } else { - let mut index = self.alt_nick_index.write().unwrap(); + let mut index = self.alt_nick_index.write(); + if self.config().should_ghost() && *index != 0 { for seq in &self.config().ghost_sequence() { self.send(NICKSERV(format!( @@ -406,9 +625,11 @@ impl ClientState { *index = 0; 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() + ))) } } @@ -417,14 +638,17 @@ impl ClientState { Ok(()) } else { self.send_mode( - self.current_nickname(), &Mode::as_user_modes(self.config().umodes()).map_err(|e| { - error::IrcError::InvalidMessage { + self.current_nickname(), + &Mode::as_user_modes(self.config().umodes()).map_err(|e| { + error::Error::InvalidMessage { string: format!( - "MODE {} {}", self.current_nickname(), self.config().umodes() + "MODE {} {}", + self.current_nickname(), + self.config().umodes() ), cause: e, } - })? + })?, ) } } @@ -434,7 +658,7 @@ impl ClientState { #[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 let Some(vec) = self.chanlists.write().get_mut(&chan.to_owned()) { if !src.is_empty() { vec.push(User::new(src)) } @@ -446,7 +670,7 @@ impl ClientState { #[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 let Some(vec) = self.chanlists.write().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); @@ -463,12 +687,10 @@ impl ClientState { 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); - } + + for vec in self.chanlists.write().values_mut() { + if let Some(p) = vec.iter().position(|x| x.get_nickname() == src) { + vec.swap_remove(p); } } } @@ -481,13 +703,11 @@ impl ClientState { 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; - } + + for (_, vec) in self.chanlists.write().iter_mut() { + if let Some(n) = vec.iter().position(|x| x.get_nickname() == old_nick) { + let new_entry = User::new(new_nick); + vec[n] = new_entry; } } } @@ -500,7 +720,7 @@ impl ClientState { 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(vec) = self.chanlists.write().get_mut(chan) { if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) { vec[n].update_access_level(mode) } @@ -520,8 +740,8 @@ impl ClientState { if args.len() == 3 { let chan = &args[2]; for user in users.split(' ') { - let mut chanlists = self.chanlists.lock().unwrap(); - chanlists + self.chanlists + .write() .entry(chan.clone()) .or_insert_with(Vec::new) .push(User::new(user)) @@ -547,10 +767,7 @@ impl ClientState { } 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, &format!("SOURCE {}", self.config().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") { @@ -571,54 +788,205 @@ impl ClientState { fn handle_ctcp(&self, _: &str, _: &[&str]) -> error::Result<()> { Ok(()) } + + pub_state_base!(); +} + +/// Thread-safe sender that can be used with the client. +#[derive(Debug, Clone)] +pub struct Sender { + tx_outgoing: UnboundedSender, +} + +impl Sender { + /// Send a single message to the unbounded queue. + pub fn send>(&self, msg: M) -> error::Result<()> { + Ok(self.tx_outgoing.unbounded_send(msg.into())?) + } + + pub_state_base!(); + pub_sender_base!(); +} + +/// Future to handle outgoing messages. +/// +/// Note: this is essentially the same as a version of [SendAll](https://github.com/rust-lang-nursery/futures-rs/blob/master/futures-util/src/sink/send_all.rs) that owns it's sink and stream. +#[derive(Debug)] +pub struct Outgoing { + sink: SplitSink, + stream: UnboundedReceiver, + buffered: Option, +} + +impl Outgoing { + fn try_start_send( + &mut self, + cx: &mut Context<'_>, + message: Message, + ) -> Poll> { + debug_assert!(self.buffered.is_none()); + + match Pin::new(&mut self.sink).poll_ready(cx)? { + Poll::Ready(()) => Poll::Ready(Pin::new(&mut self.sink).start_send(message)), + Poll::Pending => { + self.buffered = Some(message); + Poll::Pending + } + } + } +} + +impl FusedFuture for Outgoing { + fn is_terminated(&self) -> bool { + // NB: outgoing stream never terminates. + // TODO: should it terminate if rx_outgoing is terminated? + false + } +} + +impl Future for Outgoing { + type Output = error::Result<()>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = &mut *self; + + if let Some(message) = this.buffered.take() { + ready!(this.try_start_send(cx, message))? + } + + loop { + match this.stream.poll_next_unpin(cx) { + Poll::Ready(Some(message)) => ready!(this.try_start_send(cx, message))?, + Poll::Ready(None) => { + ready!(Pin::new(&mut this.sink).poll_flush(cx))?; + return Poll::Ready(Ok(())); + } + Poll::Pending => { + ready!(Pin::new(&mut this.sink).poll_flush(cx))?; + return Poll::Pending; + } + } + } + } } /// The canonical implementation of a connection to an IRC server. /// -/// The type itself provides a number of methods to create new connections, but most of the API -/// surface is in the form of the [`Client`](trait.Client.html) and -/// [`ClientExt`](./ext/trait.ClientExt.html) traits that provide methods of communicating with -/// the server after connection. Cloning an `IrcClient` is relatively cheap, as it's equivalent to -/// cloning a single `Arc`. This may be useful for setting up multiple threads with access to one -/// connection. -/// /// For a full example usage, see [`irc::client`](./index.html). -#[derive(Clone, Debug)] -pub struct IrcClient { +#[derive(Debug)] +pub struct Client { /// The internal, thread-safe server state. state: Arc, + incoming: Option>, + outgoing: Option, + sender: Sender, + #[cfg(test)] /// A view of the logs for a mock connection. - view: Option, + view: Option, } -impl Client for IrcClient { +impl Client { + /// Creates a new `Client` from the configuration at the specified path, connecting + /// immediately. This function is short-hand for loading the configuration and then calling + /// `Client::from_config` and consequently inherits its behaviors. + /// + /// # Example + /// ```no_run + /// # use irc::client::prelude::*; + /// # #[tokio::main] + /// # async fn main() -> irc::error::Result<()> { + /// let client = Client::new("config.toml").await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn new>(config: P) -> error::Result { + Client::from_config(Config::load(config)?).await + } + + /// Creates a `Future` of an `Client` from the specified configuration and on the event loop + /// corresponding to the given handle. This can be used to set up a number of `Clients` on a + /// single, shared event loop. It can also be used to take more control over execution and error + /// handling. Connection will not occur until the event loop is run. + pub async fn from_config(config: Config) -> error::Result { + let (tx_outgoing, rx_outgoing) = mpsc::unbounded(); + let conn = Connection::new(&config, tx_outgoing.clone()).await?; + + #[cfg(test)] + let view = conn.log_view(); + + let (sink, incoming) = conn.split(); + + let sender = Sender { tx_outgoing }; + + Ok(Client { + sender: sender.clone(), + state: Arc::new(ClientState::new(sender, config)), + incoming: Some(incoming), + outgoing: Some(Outgoing { + sink, + stream: rx_outgoing, + buffered: None, + }), + #[cfg(test)] + view, + }) + } + + /// Gets the log view from the internal transport. Only used for unit testing. + #[cfg(test)] + fn log_view(&self) -> &self::transport::LogView { + self.view + .as_ref() + .expect("there should be a log during testing") + } + + /// Take the outgoing future in order to drive it yourself. + /// + /// Must be called before `stream` if you intend to drive this future + /// yourself. + pub fn outgoing(&mut self) -> Option { + self.outgoing.take() + } + + /// Get access to a thread-safe sender that can be used with the client. + pub fn sender(&self) -> Sender { + self.sender.clone() + } + + /// Gets the configuration being used with this `Client`. fn config(&self) -> &Config { &self.state.config } - fn send>(&self, msg: M) -> error::Result<()> - where - Self: Sized, - { - self.state.send(msg) - } + /// Gets a stream of incoming messages from the `Client`'s connection. This is only necessary + /// when trying to set up more complex clients, and requires use of the `futures` crate. Most + /// IRC bots should be able to get by using only `for_each_incoming` to handle received + /// messages. You can find some examples of more complex setups using `stream` in the + /// [GitHub repository](https://github.com/aatxe/irc/tree/stable/examples). + /// + /// **Note**: The stream can only be returned once. Subsequent attempts will cause a panic. + // FIXME: when impl traits stabilize, we should change this return type. + pub fn stream(&mut self) -> error::Result { + let stream = self + .incoming + .take() + .ok_or_else(|| error::Error::StreamAlreadyConfigured)?; - fn stream(&self) -> ClientStream { - ClientStream { + Ok(ClientStream { state: Arc::clone(&self.state), - stream: self.state.incoming.lock().unwrap().take().expect( - "Stream was already obtained once, and cannot be reobtained." - ), - } + stream, + outgoing: self.outgoing.take(), + }) } + /// Gets a list of currently joined channels. This will be `None` if tracking is disabled + /// altogether via the `nochanlists` feature. #[cfg(not(feature = "nochanlists"))] - fn list_channels(&self) -> Option> { + pub fn list_channels(&self) -> Option> { Some( self.state .chanlists - .lock() - .unwrap() + .read() .keys() .map(|k| k.to_owned()) .collect(), @@ -626,145 +994,38 @@ impl Client for IrcClient { } #[cfg(feature = "nochanlists")] - fn list_channels(&self) -> Option> { + pub fn list_channels(&self) -> Option> { None } + /// Gets a list of [`Users`](./data/user/struct.User.html) in the specified channel. If the + /// specified channel hasn't been joined or the `nochanlists` feature is enabled, this function + /// will return `None`. + /// + /// For best results, be sure to request `multi-prefix` support from the server. This will allow + /// for more accurate tracking of user rank (e.g. oper, half-op, etc.). + /// # Requesting multi-prefix support + /// ```no_run + /// # use irc::client::prelude::*; + /// use irc::proto::caps::Capability; + /// + /// # #[tokio::main] + /// # async fn main() -> irc::error::Result<()> { + /// # let client = Client::new("config.toml").await?; + /// client.send_cap_req(&[Capability::MultiPrefix])?; + /// client.identify()?; + /// # Ok(()) + /// # } + /// ``` #[cfg(not(feature = "nochanlists"))] - fn list_users(&self, chan: &str) -> Option> { - self.state - .chanlists - .lock() - .unwrap() - .get(&chan.to_owned()) - .cloned() + pub fn list_users(&self, chan: &str) -> Option> { + self.state.chanlists.read().get(&chan.to_owned()).cloned() } #[cfg(feature = "nochanlists")] - fn list_users(&self, _: &str) -> Option> { + pub fn list_users(&self, _: &str) -> Option> { None } -} - -impl IrcClient { - /// Creates a new `IrcClient` from the configuration at the specified path, connecting - /// immediately. This function is short-hand for loading the configuration and then calling - /// `IrcClient::from_config` and consequently inherits its behaviors. - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use irc::client::prelude::*; - /// # fn main() { - /// let client = IrcClient::new("config.toml").unwrap(); - /// # } - /// ``` - pub fn new>(config: P) -> error::Result { - IrcClient::from_config(Config::load(config)?) - } - - /// Creates a new `IrcClient` from the specified configuration, connecting immediately. Due to - /// current design limitations, error handling here is somewhat limited. In particular, failed - /// connections will cause the program to panic because the connection attempt is made on a - /// freshly created thread. If you need to avoid this behavior and handle errors more - /// gracefully, it is recommended that you use an - /// [`IrcReactor`](./reactor/struct.IrcReactor.html) instead. - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use std::default::Default; - /// # use irc::client::prelude::*; - /// # fn main() { - /// let config = Config { - /// nickname: Some("example".to_owned()), - /// server: Some("irc.example.com".to_owned()), - /// .. Default::default() - /// }; - /// let client = IrcClient::from_config(config).unwrap(); - /// # } - /// ``` - pub fn from_config(config: Config) -> error::Result { - // 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 = Runtime::new().unwrap(); - // Setting up internal processing stuffs. - let conn = reactor.block_on(Connection::new(cfg.clone()).unwrap()).unwrap(); - - tx_view.send(conn.log_view()).unwrap(); - let (sink, stream) = conn.split(); - - let outgoing_future = sink.send_all(rx_outgoing.map_err::(|_| { - unreachable!("futures::sync::mpsc::Receiver should never return Err"); - })).map(|_| ()).map_err(|e| panic!("{}", e)); - - // Send the stream half back to the original thread. - tx_incoming.send(stream).unwrap(); - - reactor.block_on(outgoing_future).unwrap(); - }); - - Ok(IrcClient { - state: Arc::new(ClientState::new(rx_incoming.wait()?, tx_outgoing, config)), - view: rx_view.wait()?, - }) - } - - /// Creates a `Future` of an `IrcClient` from the specified configuration and on the event loop - /// corresponding to the given handle. This can be used to set up a number of `IrcClients` on a - /// single, shared event loop. It can also be used to take more control over execution and error - /// handling. Connection will not occur until the event loop is run. - /// - /// Proper usage requires familiarity with `tokio` and `futures`. You can find more information - /// in the crate documentation for [`tokio-core`](http://docs.rs/tokio-core) or - /// [`futures`](http://docs.rs/futures). Additionally, you can find detailed tutorials on using - /// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). An easy - /// to use abstraction that does not require this knowledge is available via - /// [`IrcReactors`](./reactor/struct.IrcReactor.html). - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # extern crate tokio; - /// # use std::default::Default; - /// # use irc::client::prelude::*; - /// # use irc::client::PackedIrcClient; - /// # use irc::error; - /// # use tokio::runtime::current_thread::Runtime; - /// # fn main() { - /// # let config = Config { - /// # nickname: Some("example".to_owned()), - /// # server: Some("irc.example.com".to_owned()), - /// # .. Default::default() - /// # }; - /// let mut reactor = Runtime::new().unwrap(); - /// let future = IrcClient::new_future(config).unwrap(); - /// // immediate connection errors (like no internet) will turn up here... - /// let PackedIrcClient(client, future) = reactor.block_on(future).unwrap(); - /// // runtime errors (like disconnections and so forth) will turn up here... - /// reactor.block_on(client.stream().for_each(move |irc_msg| { - /// // processing messages works like usual - /// process_msg(&client, irc_msg) - /// }).join(future)).unwrap(); - /// # } - /// # fn process_msg(server: &IrcClient, message: Message) -> error::Result<()> { Ok(()) } - /// ``` - pub fn new_future(config: Config) -> error::Result { - let (tx_outgoing, rx_outgoing) = mpsc::unbounded(); - - Ok(IrcClientFuture { - conn: Connection::new(config.clone())?, - config: config, - tx_outgoing: Some(tx_outgoing), - rx_outgoing: Some(rx_outgoing), - }) - } /// Gets the current nickname in use. This may be the primary username set in the configuration, /// or it could be any of the alternative nicknames listed as well. As a result, this is the @@ -773,76 +1034,59 @@ impl IrcClient { 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() + /// Sends a [`Command`](../proto/command/enum.Command.html) as this `Client`. This is the + /// core primitive for sending messages to the server. + /// + /// # Example + /// ```no_run + /// # use irc::client::prelude::*; + /// # #[tokio::main] + /// # async fn main() { + /// # let client = Client::new("config.toml").await.unwrap(); + /// client.send(Command::NICK("example".to_owned())).unwrap(); + /// client.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap(); + /// # } + /// ``` + pub fn send>(&self, msg: M) -> error::Result<()> { + self.sender.send(msg) } -} -/// A future representing the eventual creation of an `IrcClient`. -/// -/// Interaction with this future relies on the `futures` API, but is only expected for more advanced -/// use cases. To learn more, you can view the documentation for the -/// [`futures`](https://docs.rs/futures/) crate, or the tutorials for -/// [`tokio`](https://tokio.rs/docs/getting-started/futures/). An easy to use abstraction that does -/// not require this knowledge is available via [`IrcReactors`](./reactor/struct.IrcReactor.html). -#[derive(Debug)] -pub struct IrcClientFuture { - conn: ConnectionFuture, - config: Config, - tx_outgoing: Option>, - rx_outgoing: Option>, -} - -impl Future for IrcClientFuture { - type Item = PackedIrcClient; - type Error = error::IrcError; - - fn poll(&mut self) -> Poll { - 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::(|()| { - 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)))) + /// Sends a CAP END, NICK and USER to identify. + pub fn identify(&self) -> error::Result<()> { + // Send a CAP END to signify that we're IRCv3-compliant (and to end negotiations!). + self.send(CAP(None, END, None, None))?; + if self.config().password() != "" { + self.send(PASS(self.config().password().to_owned()))?; + } + self.send(NICK(self.config().nickname()?.to_owned()))?; + self.send(USER( + self.config().username().to_owned(), + "0".to_owned(), + self.config().real_name().to_owned(), + ))?; + Ok(()) } -} -/// An `IrcClient` packaged with a future that drives its message sending. In order for the client -/// to actually work properly, this future _must_ be running. -/// -/// This type should only be used by advanced users who are familiar with the implementation of this -/// crate. An easy to use abstraction that does not require this knowledge is available via -/// [`IrcReactors`](./reactor/struct.IrcReactor.html). -pub struct PackedIrcClient(pub IrcClient, pub Box + Send + 'static>); + pub_state_base!(); + pub_sender_base!(); +} #[cfg(test)] mod test { - use std::collections::HashMap; - use std::default::Default; - use std::thread; - use std::time::Duration; + use std::{collections::HashMap, default::Default, thread, time::Duration}; - use super::{IrcClient, Client}; - use error::IrcError; - use client::data::Config; + use super::Client; #[cfg(not(feature = "nochanlists"))] - use client::data::User; - use proto::{ChannelMode, IrcCodec, Mode}; - use proto::command::Command::{PART, PRIVMSG, Raw}; + use crate::client::data::User; + use crate::{ + client::data::Config, + error::Error, + proto::{ + command::Command::{Raw, PRIVMSG}, + ChannelMode, IrcCodec, Mode, + }, + }; + use futures::prelude::*; pub fn test_config() -> Config { Config { @@ -857,73 +1101,79 @@ mod test { } } - pub fn get_client_value(client: IrcClient) -> String { + pub fn get_client_value(client: Client) -> 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)); - client.log_view().sent().unwrap().iter().fold(String::new(), |mut acc, msg| { - // NOTE: we have to sanitize here because sanitization happens in IrcCodec after the - // messages are converted into strings, but our transport logger catches messages before - // they ever reach that point. - acc.push_str(&IrcCodec::sanitize(msg.to_string())); - acc - }) + client + .log_view() + .sent() + .unwrap() + .iter() + .fold(String::new(), |mut acc, msg| { + // NOTE: we have to sanitize here because sanitization happens in IrcCodec after the + // messages are converted into strings, but our transport logger catches messages before + // they ever reach that point. + acc.push_str(&IrcCodec::sanitize(msg.to_string())); + acc + }) } - #[test] - fn stream() { + #[tokio::test] + async fn stream() -> Result<(), failure::Error> { 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 { + + let mut client = Client::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); + }) + .await?; + + client.stream()?.collect().await?; + // assert_eq!(&messages[..], exp); + Ok(()) } - #[test] - fn handle_message() { + #[tokio::test] + async fn handle_message() -> Result<(), failure::Error> { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "JOIN #test\r\nJOIN #test2\r\n" ); + Ok(()) } - #[test] - fn handle_end_motd_with_nick_password() { + #[tokio::test] + async fn handle_end_motd_with_nick_password() -> Result<(), failure::Error> { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::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(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NICKSERV IDENTIFY password\r\nJOIN #test\r\n\ - JOIN #test2\r\n" + JOIN #test2\r\n" ); + Ok(()) } - #[test] - fn handle_end_motd_with_chan_keys() { + #[tokio::test] + async fn handle_end_motd_with_chan_keys() -> Result<(), failure::Error> { let value = ":irc.test.net 376 test :End of /MOTD command\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some(format!("test")), channels: Some(vec![format!("#test"), format!("#test2")]), @@ -933,21 +1183,21 @@ mod test { Some(map) }, ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "JOIN #test\r\nJOIN #test2 password\r\n" ); + Ok(()) } - #[test] - fn handle_end_motd_with_ghost() { + #[tokio::test] + async fn handle_end_motd_with_ghost() -> Result<(), failure::Error> { 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 { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some(format!("test")), alt_nicks: Some(vec![format!("test2")]), @@ -955,22 +1205,22 @@ mod test { channels: Some(vec![format!("#test"), format!("#test2")]), should_ghost: Some(true), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; 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" + NICK :test\r\nNICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n" ); + Ok(()) } - #[test] - fn handle_end_motd_with_ghost_seq() { + #[tokio::test] + async fn handle_end_motd_with_ghost_seq() -> Result<(), failure::Error> { 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 { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some(format!("test")), alt_nicks: Some(vec![format!("test2")]), @@ -979,164 +1229,176 @@ mod test { should_ghost: Some(true), ghost_sequence: Some(vec![format!("RECOVER"), format!("RELEASE")]), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; 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" + \r\nNICKSERV RELEASE test password\r\nNICK :test\r\nNICKSERV IDENTIFY password\ + \r\nJOIN #test\r\nJOIN #test2\r\n" ); + Ok(()) } - #[test] - fn handle_end_motd_with_umodes() { + #[tokio::test] + async fn handle_end_motd_with_umodes() -> Result<(), failure::Error> { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::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(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "MODE test +B\r\nJOIN #test\r\nJOIN #test2\r\n" ); + Ok(()) } - #[test] - fn nickname_in_use() { + #[tokio::test] + async fn nickname_in_use() -> Result<(), failure::Error> { let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], "NICK :test2\r\n"); + Ok(()) } - #[test] - fn ran_out_of_nicknames() { + #[tokio::test] + async fn ran_out_of_nicknames() -> Result<(), failure::Error> { 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 { + let mut client = Client::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 { + }) + .await?; + let res = client.stream()?.try_collect::>().await; + if let Err(Error::NoUsableNick) = res { () } else { panic!("expected error when no valid nicks were specified") } + Ok(()) } - #[test] - fn send() { - let client = IrcClient::from_config(test_config()).unwrap(); - assert!( - client - .send(PRIVMSG(format!("#test"), format!("Hi there!"))) - .is_ok() - ); + #[tokio::test] + async fn send() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + assert!(client + .send(PRIVMSG(format!("#test"), format!("Hi there!"))) + .is_ok()); + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "PRIVMSG #test :Hi there!\r\n" ); + Ok(()) } - #[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() + #[tokio::test] + async fn send_no_newline_injection() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + assert!(client + .send(PRIVMSG(format!("#test"), format!("Hi there!\r\nJOIN #bad"))) + .is_ok()); + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG #test :Hi there!\r\n" ); - assert_eq!(&get_client_value(client)[..], "PRIVMSG #test :Hi there!\r\n"); + Ok(()) } - #[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() + #[tokio::test] + async fn send_raw_is_really_raw() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + 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()); + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PASS password\r\nNICK test\r\n" ); - 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"); + Ok(()) } - #[test] + #[tokio::test] #[cfg(not(feature = "nochanlists"))] - fn channel_tracking_names() { + async fn channel_tracking_names() -> Result<(), failure::Error> { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::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()]) + }) + .await?; + client.stream()?.collect().await?; + assert_eq!(client.list_channels().unwrap(), vec!["#test".to_owned()]); + Ok(()) } - #[test] + // TODO: this seems to require a somewhat more complex interaction with the + // client. + /* + #[tokio::test] #[cfg(not(feature = "nochanlists"))] - fn channel_tracking_names_part() { + async fn channel_tracking_names_part() -> Result<(), failure::Error> { + use crate::proto::command::Command::PART; + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert!(client.send(PART(format!("#test"), None)).is_ok()); - assert!(client.list_channels().unwrap().is_empty()) + assert!(client.list_channels().unwrap().is_empty()); + Ok(()) } + */ - #[test] + #[tokio::test] #[cfg(not(feature = "nochanlists"))] - fn user_tracking_names() { + async fn user_tracking_names() -> Result<(), failure::Error> { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![User::new("test"), User::new("~owner"), User::new("&admin")] - ) + ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(not(feature = "nochanlists"))] - fn user_tracking_names_join() { + async fn user_tracking_names_join() -> Result<(), failure::Error> { 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 { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![ @@ -1145,60 +1407,57 @@ mod test { User::new("&admin"), User::new("test2"), ] - ) + ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(not(feature = "nochanlists"))] - fn user_tracking_names_kick() { + async fn user_tracking_names_kick() -> Result<(), failure::Error> { 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 { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), - vec![ - User::new("&admin"), - User::new("~owner"), - ] - ) + vec![User::new("&admin"), User::new("~owner"),] + ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(not(feature = "nochanlists"))] - fn user_tracking_names_part() { + async fn user_tracking_names_part() -> Result<(), failure::Error> { 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 { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![User::new("test"), User::new("&admin")] - ) + ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(not(feature = "nochanlists"))] - fn user_tracking_names_mode() { + async fn user_tracking_names_mode() -> Result<(), failure::Error> { 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 { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( client.list_users("#test").unwrap(), vec![User::new("@test"), User::new("~owner"), User::new("&admin")] @@ -1214,153 +1473,462 @@ mod test { 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()); + Ok(()) } - #[test] + #[tokio::test] #[cfg(feature = "nochanlists")] - fn no_user_tracking() { + async fn no_user_tracking() -> Result<(), failure::Error> { let value = ":irc.test.net 353 test = #test :test ~owner &admin"; - let client = IrcClient::from_config(Config { + let mut client = Client::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()) + }) + .await?; + client.stream()?.collect().await?; + assert!(client.list_users("#test").is_none()); + Ok(()) } - #[test] - fn handle_single_soh() { + #[tokio::test] + async fn handle_single_soh() -> Result<(), failure::Error> { let value = ":test!test@test PRIVMSG #test :\u{001}\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some(format!("test")), channels: Some(vec![format!("#test"), format!("#test2")]), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; + Ok(()) } - - #[test] + #[tokio::test] #[cfg(feature = "ctcp")] - fn finger_response() { + async fn finger_response() -> Result<(), failure::Error> { let value = ":test!test@test PRIVMSG test :\u{001}FINGER\u{001}\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE test :\u{001}FINGER :test (test)\u{001}\r\n" ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(feature = "ctcp")] - fn version_response() { + async fn version_response() -> Result<(), failure::Error> { let value = ":test!test@test PRIVMSG test :\u{001}VERSION\u{001}\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], &format!( "NOTICE test :\u{001}VERSION {}\u{001}\r\n", - ::VERSION_STR, + crate::VERSION_STR, ) ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(feature = "ctcp")] - fn source_response() { + async fn source_response() -> Result<(), failure::Error> { let value = ":test!test@test PRIVMSG test :\u{001}SOURCE\u{001}\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE test :\u{001}SOURCE https://github.com/aatxe/irc\u{001}\r\n" ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(feature = "ctcp")] - fn ctcp_ping_response() { + async fn ctcp_ping_response() -> Result<(), failure::Error> { let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE test :\u{001}PING test\u{001}\r\n" ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(feature = "ctcp")] - fn time_response() { + async fn time_response() -> Result<(), failure::Error> { let value = ":test!test@test PRIVMSG test :\u{001}TIME\u{001}\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; let val = get_client_value(client); assert!(val.starts_with("NOTICE test :\u{001}TIME :")); assert!(val.ends_with("\u{001}\r\n")); + Ok(()) } - #[test] + #[tokio::test] #[cfg(feature = "ctcp")] - fn user_info_response() { + async fn user_info_response() -> Result<(), failure::Error> { let value = ":test!test@test PRIVMSG test :\u{001}USERINFO\u{001}\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!( &get_client_value(client)[..], "NOTICE test :\u{001}USERINFO :Testing.\u{001}\ - \r\n" + \r\n" ); + Ok(()) } - #[test] + #[tokio::test] #[cfg(feature = "ctcp")] - fn ctcp_ping_no_timestamp() { + async fn ctcp_ping_no_timestamp() -> Result<(), failure::Error> { let value = ":test!test@test PRIVMSG test :\u{001}PING\u{001}\r\n"; - let client = IrcClient::from_config(Config { + let mut client = Client::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() - }).unwrap(); - client.for_each_incoming(|message| { - println!("{:?}", message); - }).unwrap(); + }) + .await?; + client.stream()?.collect().await?; assert_eq!(&get_client_value(client)[..], ""); + Ok(()) + } + + #[tokio::test] + async fn identify() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.identify()?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "CAP END\r\nNICK :test\r\n\ + USER test 0 * :test\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn identify_with_password() -> Result<(), failure::Error> { + let mut client = Client::from_config(Config { + nickname: Some(format!("test")), + password: Some(format!("password")), + ..test_config() + }) + .await?; + client.identify()?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "CAP END\r\nPASS :password\r\nNICK :test\r\n\ + USER test 0 * :test\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn send_pong() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_pong("irc.test.net")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "PONG :irc.test.net\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_join() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_join("#test,#test2,#test3")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "JOIN #test,#test2,#test3\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn send_part() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_part("#test")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "PART #test\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_oper() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_oper("test", "test")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "OPER test :test\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_privmsg() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_privmsg("#test", "Hi, everybody!")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG #test :Hi, everybody!\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn send_notice() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_notice("#test", "Hi, everybody!")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "NOTICE #test :Hi, everybody!\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn send_topic_no_topic() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_topic("#test", "")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "TOPIC #test\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_topic() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_topic("#test", "Testing stuff.")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "TOPIC #test :Testing stuff.\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn send_kill() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_kill("test", "Testing kills.")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "KILL test :Testing kills.\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn send_kick_no_message() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_kick("#test", "test", "")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "KICK #test test\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_kick() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_kick("#test", "test", "Testing kicks.")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "KICK #test test :Testing kicks.\r\n" + ); + Ok(()) + } + + #[tokio::test] + async fn send_mode_no_modeparams() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)])?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "MODE #test +i\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_mode() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_mode( + "#test", + &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))], + )?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "MODE #test +o test\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_samode_no_modeparams() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_samode("#test", "+i", "")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "SAMODE #test +i\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_samode() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_samode("#test", "+o", "test")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "SAMODE #test +o test\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_sanick() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_sanick("test", "test2")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "SANICK test test2\r\n"); + Ok(()) + } + + #[tokio::test] + async fn send_invite() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_invite("test", "#test")?; + client.stream()?.collect().await?; + assert_eq!(&get_client_value(client)[..], "INVITE test #test\r\n"); + Ok(()) + } + + #[tokio::test] + #[cfg(feature = "ctcp")] + async fn send_ctcp() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_ctcp("test", "MESSAGE")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG test :\u{001}MESSAGE\u{001}\r\n" + ); + Ok(()) + } + + #[tokio::test] + #[cfg(feature = "ctcp")] + async fn send_action() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_action("test", "tests.")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n" + ); + Ok(()) + } + + #[tokio::test] + #[cfg(feature = "ctcp")] + async fn send_finger() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_finger("test")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG test :\u{001}FINGER\u{001}\r\n" + ); + Ok(()) + } + + #[tokio::test] + #[cfg(feature = "ctcp")] + async fn send_version() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_version("test")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG test :\u{001}VERSION\u{001}\r\n" + ); + Ok(()) + } + + #[tokio::test] + #[cfg(feature = "ctcp")] + async fn send_source() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_source("test")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG test :\u{001}SOURCE\u{001}\r\n" + ); + Ok(()) + } + + #[tokio::test] + #[cfg(feature = "ctcp")] + async fn send_user_info() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_user_info("test")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG test :\u{001}USERINFO\u{001}\r\n" + ); + Ok(()) + } + + #[tokio::test] + #[cfg(feature = "ctcp")] + async fn send_ctcp_ping() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_ctcp_ping("test")?; + client.stream()?.collect().await?; + let val = get_client_value(client); + println!("{}", val); + assert!(val.starts_with("PRIVMSG test :\u{001}PING ")); + assert!(val.ends_with("\u{001}\r\n")); + Ok(()) + } + + #[tokio::test] + #[cfg(feature = "ctcp")] + async fn send_time() -> Result<(), failure::Error> { + let mut client = Client::from_config(test_config()).await?; + client.send_time("test")?; + client.stream()?.collect().await?; + assert_eq!( + &get_client_value(client)[..], + "PRIVMSG test :\u{001}TIME\u{001}\r\n" + ); + Ok(()) } } diff --git a/src/client/prelude.rs b/src/client/prelude.rs index bda4efa..6818526 100644 --- a/src/client/prelude.rs +++ b/src/client/prelude.rs @@ -21,11 +21,10 @@ //! dealing with IRC channel and user modes. They appear in methods for sending mode commands, //! as well as in the parsed form of received mode commands. -pub use client::data::Config; -pub use client::reactor::IrcReactor; -pub use client::{EachIncomingExt, IrcClient, Client}; -pub use client::ext::ClientExt; -pub use proto::{Capability, ChannelExt, Command, Message, Prefix, NegotiationVersion, Response}; -pub use proto::{ChannelMode, Mode, UserMode}; - -pub use futures::{Future, Stream}; +pub use crate::{ + client::{data::Config, Client, Sender}, + proto::{ + Capability, ChannelExt, ChannelMode, Command, Message, Mode, NegotiationVersion, Prefix, + Response, UserMode, + }, +}; diff --git a/src/client/reactor.rs b/src/client/reactor.rs deleted file mode 100644 index 13c897b..0000000 --- a/src/client/reactor.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! A system for creating and managing IRC client connections. -//! -//! This API provides the ability to create and manage multiple IRC clients that can run on the same -//! thread through the use of a shared event loop. It can also be used to encapsulate the dependency -//! on `tokio` and `futures` in the use of `IrcClient::new_future`. This means that knowledge of -//! those libraries should be unnecessary for the average user. Nevertheless, this API also provides -//! some escape hatches that let advanced users take further advantage of these dependencies. -//! -//! # Example -//! ```no_run -//! # extern crate irc; -//! # use std::default::Default; -//! use irc::client::prelude::*; -//! use irc::error; -//! -//! fn main() { -//! let config = Config::default(); -//! let mut reactor = IrcReactor::new().unwrap(); -//! let client = reactor.prepare_client_and_connect(config).unwrap(); -//! reactor.register_client_with_handler(client, process_msg); -//! reactor.run().unwrap(); -//! } -//! # fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { Ok(()) } -//! ``` - -use futures::{Future, IntoFuture, Stream}; -use futures::future; -use tokio::runtime::current_thread as tokio_rt; - -use client::data::Config; -use client::{IrcClient, IrcClientFuture, PackedIrcClient, Client}; -use error; -use proto::Message; - -/// A thin wrapper over an event loop. -/// -/// An IRC reactor is used to create new IRC clients and to drive the management of all connected -/// clients as the application runs. It can be used to run multiple clients on the same thread, as -/// well as to get better control over error management in an IRC client. -/// -/// For a full example usage, see [`irc::client::reactor`](./index.html). -pub struct IrcReactor { - inner: tokio_rt::Runtime, - handlers: Vec>>, -} - -impl IrcReactor { - /// Creates a new reactor. - pub fn new() -> error::Result { - Ok(IrcReactor { - inner: tokio_rt::Runtime::new()?, - handlers: Vec::new(), - }) - } - - /// Creates a representation of an IRC client that has not yet attempted to connect. In - /// particular, this representation is as a `Future` that when run will produce a connected - /// [`IrcClient`](../struct.IrcClient.html). - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use std::default::Default; - /// # use irc::client::prelude::*; - /// # fn main() { - /// let future_client = IrcReactor::new().and_then(|mut reactor| { - /// reactor.prepare_client(Config::default()) - /// }); - /// # } - /// ``` - pub fn prepare_client(&mut self, config: Config) -> error::Result { - IrcClient::new_future(config) - } - - /// Runs an [`IrcClientFuture`](../struct.IrcClientFuture.html), such as one from - /// `prepare_client` to completion, yielding an [`IrcClient`](../struct.IrcClient.html). - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use std::default::Default; - /// # use irc::client::prelude::*; - /// # fn main() { - /// let client = IrcReactor::new().and_then(|mut reactor| { - /// reactor.prepare_client(Config::default()).and_then(|future| { - /// reactor.connect_client(future) - /// }) - /// }); - /// # } - /// ``` - pub fn connect_client(&mut self, future: IrcClientFuture) -> error::Result { - self.inner.block_on(future).map(|PackedIrcClient(client, future)| { - self.register_future(future); - client - }) - } - - /// Creates a new [`IrcClient`](../struct.IrcClient.html) from the specified configuration, - /// connecting immediately. This is guaranteed to be the composition of `prepare_client` and - /// `connect_client`. - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use std::default::Default; - /// # use irc::client::prelude::*; - /// # fn main() { - /// let client = IrcReactor::new().and_then(|mut reactor| { - /// reactor.prepare_client_and_connect(Config::default()) - /// }); - /// # } - /// ``` - pub fn prepare_client_and_connect(&mut self, config: Config) -> error::Result { - self.prepare_client(config).and_then(|future| self.connect_client(future)) - } - - /// Registers the given client with the specified message handler. The reactor will store this - /// setup until the next call to run, where it will be used to process new messages over the - /// connection indefinitely (or until failure). As registration is consumed by `run`, subsequent - /// calls to run will require new registration. - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use std::default::Default; - /// # use irc::client::prelude::*; - /// # fn main() { - /// # let config = Config::default(); - /// let mut reactor = IrcReactor::new().unwrap(); - /// let client = reactor.prepare_client_and_connect(config).unwrap(); - /// reactor.register_client_with_handler(client, |client, msg| { - /// // Message processing happens here. - /// Ok(()) - /// }) - /// # } - /// ``` - pub fn register_client_with_handler( - &mut self, client: IrcClient, mut handler: F - ) where F: FnMut(&IrcClient, Message) -> U + 'static, - U: IntoFuture + 'static, - U::Future: Send { - let handle = self.inner.handle().clone(); - self.handlers.push(Box::new(client.stream().for_each(move |message| { - handle.spawn(handler(&client, message).into_future().map_err(|_| (())))?; - - Ok(()) - }))); - } - - /// Registers an arbitrary future with this reactor. This is a sort of escape hatch that allows - /// you to take more control over what runs on the reactor without requiring you to bring in - /// additional knowledge about `tokio`. It is suspected that `register_client_with_handler` will - /// be sufficient for most use cases. - pub fn register_future( - &mut self, future: F - ) where F: IntoFuture + 'static { - self.handlers.push(Box::new(future.into_future())) - } - - /// Returns a handle to the internal event loop. This is a sort of escape hatch that allows you - /// to take more control over what runs on the reactor using `tokio`. This can be used for - /// sharing this reactor with some elements of other libraries. - pub fn inner_handle(&self) -> tokio_rt::Handle { - self.inner.handle() - } - - /// Consumes all registered handlers and futures, and runs them. When using - /// `register_client_with_handler`, this will block indefinitely (until failure occurs) as it - /// will simply continue to process new, incoming messages for each client that was registered. - /// - /// # Example - /// ```no_run - /// # extern crate irc; - /// # use std::default::Default; - /// # use irc::client::prelude::*; - /// # use irc::error; - /// # fn main() { - /// # let config = Config::default(); - /// let mut reactor = IrcReactor::new().unwrap(); - /// let client = reactor.prepare_client_and_connect(config).unwrap(); - /// reactor.register_client_with_handler(client, process_msg); - /// reactor.run().unwrap(); - /// # } - /// # fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { Ok(()) } - /// ``` - pub fn run(&mut self) -> error::Result<()> { - let mut handlers = Vec::new(); - while let Some(handler) = self.handlers.pop() { - handlers.push(handler); - } - self.inner.block_on(future::join_all(handlers).map(|_| ())) - } -} diff --git a/src/client/transport.rs b/src/client/transport.rs index aa76a79..1987f6a 100644 --- a/src/client/transport.rs +++ b/src/client/transport.rs @@ -1,241 +1,208 @@ //! An IRC transport that wraps an IRC-framed stream to provide a number of features including //! automatic PING replies, automatic sending of PINGs, and message rate-limiting. This can be used //! as the basis for implementing a more full IRC client. -use std::collections::VecDeque; -use std::sync::{Arc, RwLock, RwLockReadGuard}; -use std::time::{Duration, Instant}; +use std::{ + pin::Pin, + sync::{Arc, RwLock, RwLockReadGuard}, + task::{Context, Poll}, + time::Duration, +}; -use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream}; use chrono::prelude::*; -use tokio_codec::Framed; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer; -use tokio_timer::{Interval, Sleep, Timer}; +use futures_channel::mpsc::UnboundedSender; +use futures_util::{future::Future, ready, sink::Sink, stream::Stream}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + time::{self, Delay, Interval}, +}; +use tokio_util::codec::Framed; -use error; -use client::data::Config; -use proto::{Command, IrcCodec, Message}; +use crate::{ + client::data::Config, + error, + proto::{Command, IrcCodec, Message}, +}; -/// An IRC transport that handles core functionality for the IRC protocol. This is used in the -/// implementation of `Connection` and ultimately `IrcServer`, and plays an important role in -/// handling connection timeouts, message throttling, and ping response. -pub struct IrcTransport -where - T: AsyncRead + AsyncWrite, -{ - /// The inner connection framed with an `IrcCodec`. - inner: Framed, - /// A timer used in computing windows for message throttling. - burst_timer: Timer, - /// A queue of tasks used to implement message throttling. - rolling_burst_window: VecDeque, - /// The amount of time that each window for throttling should last (in seconds). - burst_window_length: u64, - /// The maximum number of messages that can be sent in each window. - max_burst_messages: u64, - /// The number of messages sent in the current window. - current_burst_messages: u64, - /// A timer used to determine when to send the next ping messages to the server. - ping_timer: Interval, +/// Pinger-based futures helper. +struct Pinger { + tx: UnboundedSender, /// The amount of time to wait before timing out from no ping response. - ping_timeout: u64, - /// The last data sent with a ping. - last_ping_data: String, + ping_timeout: Duration, /// The instant that the last ping was sent to the server. - last_ping_sent: Instant, - /// The instant that the last pong was received from the server. - last_pong_received: Instant, + ping_deadline: Option, + /// The interval at which to send pings. + ping_interval: Interval, } -impl IrcTransport -where - T: AsyncRead + AsyncWrite, -{ - /// Creates a new `IrcTransport` from the given IRC stream. - pub fn new(config: &Config, inner: Framed) -> IrcTransport { - let timer = tokio_timer::wheel().build(); - IrcTransport { - inner, - burst_timer: tokio_timer::wheel().build(), - rolling_burst_window: VecDeque::new(), - burst_window_length: u64::from(config.burst_window_length()), - max_burst_messages: u64::from(config.max_messages_in_burst()), - current_burst_messages: 0, - ping_timer: timer.interval(Duration::from_secs(u64::from(config.ping_time()))), - ping_timeout: u64::from(config.ping_timeout()), - last_ping_data: String::new(), - last_ping_sent: Instant::now(), - last_pong_received: Instant::now(), +impl Pinger { + /// Construct a new pinger helper. + pub fn new(tx: UnboundedSender, config: &Config) -> Pinger { + let ping_timeout = Duration::from_secs(u64::from(config.ping_timeout())); + + Self { + tx, + ping_timeout, + ping_deadline: None, + ping_interval: time::interval(ping_timeout / 2), } } - /// Gets the inner stream underlying the `IrcTransport`. - pub fn into_inner(self) -> Framed { - self.inner + /// Handle an incoming message. + fn handle_message(&mut self, message: &Message) -> error::Result<()> { + match message.command { + // On receiving a `PING` message from the server, we automatically respond with + // the appropriate `PONG` message to keep the connection alive for transport. + Command::PING(ref data, _) => { + self.send_pong(data)?; + } + // Check `PONG` responses from the server. If it matches, we will update the + // last instant that the pong was received. This will prevent timeout. + Command::PONG(_, None) | Command::PONG(_, Some(_)) => { + log::trace!("Received PONG"); + self.ping_deadline.take(); + } + _ => (), + } + + Ok(()) } - /// Determines whether or not the transport has hit the ping timeout. - fn ping_timed_out(&self) -> bool { - if self.last_pong_received < self.last_ping_sent { - self.last_ping_sent.elapsed().as_secs() >= self.ping_timeout - } else { - false - } + /// Send a pong. + fn send_pong(&mut self, data: &str) -> error::Result<()> { + self.tx + .unbounded_send(Command::PONG(data.to_owned(), None).into())?; + Ok(()) } /// Sends a ping via the transport. fn send_ping(&mut self) -> error::Result<()> { + log::trace!("Sending PING"); + // Creates new ping data using the local timestamp. - let last_ping_data = format!("{}", Local::now().timestamp()); - let data = last_ping_data.clone(); - let result = self.start_send(Command::PING(data, None).into())?; - if let AsyncSink::Ready = result { - self.poll_complete()?; - // If we succeeded in sending the ping, we will update when the last ping was sent, and - // the data that was sent with it. - self.last_ping_sent = Instant::now(); - self.last_ping_data = last_ping_data; - } + let data = format!("{}", Local::now().timestamp()); + + self.tx + .unbounded_send(Command::PING(data.clone(), None).into())?; + Ok(()) } - /// Polls the most recent burst window from the queue, always returning `NotReady` if none are - /// left for whatever reason. - fn rolling_burst_window_front(&mut self) -> Result, tokio_timer::TimerError> { - self.rolling_burst_window.front_mut().map(|w| w.poll()).unwrap_or(Ok(Async::NotReady)) - } -} - -impl Stream for IrcTransport -where - T: AsyncRead + AsyncWrite, -{ - type Item = Message; - type Error = error::IrcError; - - fn poll(&mut self) -> Poll, Self::Error> { - // If the ping timeout has been reached, we close the connection out and return an error. - if self.ping_timed_out() { - self.close()?; - return Err(error::IrcError::PingTimeout) - } - - // We poll both streams before doing any work because it is important to ensure that the - // task is correctly woken up when they are ready. - let timer_poll = self.ping_timer.poll()?; - let inner_poll = self.inner.poll()?; - - match (inner_poll, timer_poll) { - // If neither the stream nor the ping timer are ready, the transport is not ready. - (Async::NotReady, Async::NotReady) => Ok(Async::NotReady), - - // If there's nothing available yet from the stream, but the ping timer is ready, we - // simply send a ping and indicate that the transport has nothing to yield yet. - (Async::NotReady, Async::Ready(msg)) => { - assert!(msg.is_some()); - self.send_ping()?; - Ok(Async::NotReady) - } - - // If the stream yields `None`, the connection has been terminated. Thus, we don't need - // to worry about checking the ping timer, and can instead indicate that the transport - // has been terminated. - (Async::Ready(None), _) => Ok(Async::Ready(None)), - - // If we have a new message available from the stream, we'll need to do some work, and - // then yield the message. - (Async::Ready(Some(msg)), _) => { - // If the ping timer has returned, it is time to send another `PING` message! - if let Async::Ready(msg) = timer_poll { - assert!(msg.is_some()); - self.send_ping()?; - } - - match msg.command { - // On receiving a `PING` message from the server, we automatically respond with - // the appropriate `PONG` message to keep the connection alive for transport. - Command::PING(ref data, _) => { - let result = self.start_send(Command::PONG(data.to_owned(), None).into())?; - assert!(result.is_ready()); - self.poll_complete()?; - } - - // Check `PONG` responses from the server. If it matches, we will update the - // last instant that the pong was received. This will prevent timeout. - Command::PONG(ref data, None) | - Command::PONG(_, Some(ref data)) => { - if self.last_ping_data == data[..] { - self.last_pong_received = Instant::now(); - } - } - - _ => (), - } - - Ok(Async::Ready(Some(msg))) - } + /// Set the ping deadline. + fn set_deadline(&mut self) { + if self.ping_deadline.is_none() { + let ping_deadline = time::delay_for(self.ping_timeout); + self.ping_deadline = Some(ping_deadline); } } } -impl Sink for IrcTransport -where - T: AsyncRead + AsyncWrite, -{ - type SinkItem = Message; - type SinkError = error::IrcError; +impl Future for Pinger { + type Output = Result<(), error::Error>; - fn start_send(&mut self, item: Self::SinkItem) -> StartSend { - // If the ping timeout has been reached, we close the connection out and return an error. - if self.ping_timed_out() { - self.close()?; - return Err(error::IrcError::PingTimeout) - } - - // Check if the oldest message in the rolling window is discounted. - if let Async::Ready(()) = self.rolling_burst_window_front()? { - self.current_burst_messages -= 1; - self.rolling_burst_window.pop_front(); - } - - // Throttling if too many messages have been sent recently. - if self.current_burst_messages >= self.max_burst_messages { - // When throttled, we know we need to finish sending what's already queued up. - self.poll_complete()?; - return Ok(AsyncSink::NotReady(item)) - } - - match self.inner.start_send(item)? { - AsyncSink::NotReady(item) => Ok(AsyncSink::NotReady(item)), - AsyncSink::Ready => { - self.current_burst_messages += 1; - self.rolling_burst_window.push_back(self.burst_timer.sleep(Duration::from_secs( - self.burst_window_length - ))); - Ok(AsyncSink::Ready) + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if let Some(ping_deadline) = self.as_mut().ping_deadline.as_mut() { + match Pin::new(ping_deadline).poll(cx) { + Poll::Ready(()) => return Poll::Ready(Err(error::Error::PingTimeout)), + Poll::Pending => (), } } - } - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - // If the ping timeout has been reached, we close the connection out and return an error. - if self.ping_timed_out() { - self.close()?; - return Err(error::IrcError::PingTimeout) + if let Poll::Ready(_) = Pin::new(&mut self.as_mut().ping_interval).poll_next(cx) { + self.as_mut().send_ping()?; + self.as_mut().set_deadline(); } - // If it's time to send a ping, we should do it! This is necessary to ensure that the - // sink half will close even if the stream half closed without a ping timeout. - if let Async::Ready(msg) = self.ping_timer.poll()? { - assert!(msg.is_some()); - self.send_ping()?; - } + Poll::Pending + } +} - Ok(self.inner.poll_complete()?) +/// An IRC transport that handles core functionality for the IRC protocol. This is used in the +/// implementation of `Connection` and ultimately `IrcServer`, and plays an important role in +/// handling connection timeouts, message throttling, and ping response. +pub struct Transport { + /// The inner connection framed with an `IrcCodec`. + inner: Framed, + /// Helper for handle pinging. + pinger: Option, +} + +impl Unpin for Transport where T: Unpin {} + +impl Transport +where + T: Unpin + AsyncRead + AsyncWrite, +{ + /// Creates a new `Transport` from the given IRC stream. + pub fn new( + config: &Config, + inner: Framed, + tx: UnboundedSender, + ) -> Transport { + let pinger = Some(Pinger::new(tx, config)); + + Transport { inner, pinger } } - fn close(&mut self) -> Poll<(), Self::SinkError> { - self.inner.close().map_err(|e| e.into()) + /// Gets the inner stream underlying the `Transport`. + pub fn into_inner(self) -> Framed { + self.inner + } +} + +impl Stream for Transport +where + T: Unpin + AsyncRead + AsyncWrite, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if let Some(pinger) = self.as_mut().pinger.as_mut() { + match Pin::new(pinger).poll(cx) { + Poll::Ready(result) => result?, + Poll::Pending => (), + } + } + + let result = ready!(Pin::new(&mut self.as_mut().inner).poll_next(cx)); + + let message = match result { + None => return Poll::Ready(None), + Some(message) => message?, + }; + + if let Some(pinger) = self.as_mut().pinger.as_mut() { + pinger.handle_message(&message)?; + } + + Poll::Ready(Some(Ok(message))) + } +} + +impl Sink for Transport +where + T: Unpin + AsyncRead + AsyncWrite, +{ + type Error = error::Error; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + ready!(Pin::new(&mut self.as_mut().inner).poll_ready(cx))?; + Poll::Ready(Ok(())) + } + + fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + log::trace!("[SEND] {}", item); + Pin::new(&mut self.as_mut().inner).start_send(item)?; + Ok(()) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + ready!(Pin::new(&mut self.as_mut().inner).poll_flush(cx))?; + Poll::Ready(Ok(())) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + ready!(Pin::new(&mut self.as_mut().inner).poll_close(cx))?; + Poll::Ready(Ok(())) } } @@ -249,25 +216,30 @@ pub struct LogView { impl LogView { /// Gets a read guard for all the messages sent on the transport. pub fn sent(&self) -> error::Result>> { - self.sent.read().map_err(|_| error::IrcError::PoisonedLog) + self.sent.read().map_err(|_| error::Error::PoisonedLog) } /// Gets a read guard for all the messages received on the transport. pub fn received(&self) -> error::Result>> { - self.received.read().map_err(|_| error::IrcError::PoisonedLog) + self.received.read().map_err(|_| error::Error::PoisonedLog) } } -/// A logged version of the `IrcTransport` that records all sent and received messages. +/// A logged version of the `Transport` that records all sent and received messages. /// Note: this will introduce some performance overhead by cloning all messages. -pub struct Logged where T: AsyncRead + AsyncWrite { - inner: IrcTransport, +pub struct Logged { + inner: Transport, view: LogView, } -impl Logged where T: AsyncRead + AsyncWrite { - /// Wraps the given `IrcTransport` in logging. - pub fn wrap(inner: IrcTransport) -> Logged { +impl Unpin for Logged where T: Unpin {} + +impl Logged +where + T: AsyncRead + AsyncWrite, +{ + /// Wraps the given `Transport` in logging. + pub fn wrap(inner: Transport) -> Logged { Logged { inner, view: LogView { @@ -285,42 +257,55 @@ impl Logged where T: AsyncRead + AsyncWrite { impl Stream for Logged where - T: AsyncRead + AsyncWrite, + T: Unpin + AsyncRead + AsyncWrite, { - type Item = Message; - type Error = error::IrcError; + type Item = Result; - fn poll(&mut self) -> Poll, Self::Error> { - match try_ready!(self.inner.poll()) { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match ready!(Pin::new(&mut self.as_mut().inner).poll_next(cx)) { Some(msg) => { - let recv: error::Result<_> = self.view.received.write().map_err(|_| { - error::IrcError::PoisonedLog - }); - recv?.push(msg.clone()); - Ok(Async::Ready(Some(msg))) + let msg = msg?; + + self.view + .received + .write() + .map_err(|_| error::Error::PoisonedLog)? + .push(msg.clone()); + + Poll::Ready(Some(Ok(msg))) } - None => Ok(Async::Ready(None)), + None => Poll::Ready(None), } } } -impl Sink for Logged +impl Sink for Logged where - T: AsyncRead + AsyncWrite, + T: Unpin + AsyncRead + AsyncWrite, { - type SinkItem = Message; - type SinkError = error::IrcError; + type Error = error::Error; - fn start_send(&mut self, item: Self::SinkItem) -> StartSend { - let res = self.inner.start_send(item.clone())?; - let sent: error::Result<_> = self.view.sent.write().map_err(|_| { - error::IrcError::PoisonedLog - }); - sent?.push(item); - Ok(res) + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.as_mut().inner).poll_ready(cx) } - fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { - Ok(self.inner.poll_complete()?) + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.as_mut().inner).poll_close(cx) + } + + fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + Pin::new(&mut self.as_mut().inner).start_send(item.clone())?; + + self.view + .sent + .write() + .map_err(|_| error::Error::PoisonedLog)? + .push(item); + + Ok(()) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.as_mut().inner).poll_flush(cx) } } diff --git a/src/error.rs b/src/error.rs index 1284f63..e060513 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,29 +4,28 @@ use std::io::Error as IoError; use std::sync::mpsc::RecvError; use failure; -use futures::sync::mpsc::SendError; -use futures::sync::oneshot::Canceled; +use futures_channel::{ + mpsc::{SendError, TrySendError}, + oneshot::Canceled, +}; use native_tls::Error as TlsError; #[cfg(feature = "json")] use serde_json::Error as JsonError; #[cfg(feature = "yaml")] use serde_yaml::Error as YamlError; -use tokio::executor::SpawnError; -use tokio_timer::TimerError; #[cfg(feature = "toml")] use toml::de::Error as TomlReadError; #[cfg(feature = "toml")] use toml::ser::Error as TomlWriteError; -use proto::Message; -use proto::error::{ProtocolError, MessageParseError}; +use crate::proto::error::{MessageParseError, ProtocolError}; /// A specialized `Result` type for the `irc` crate. -pub type Result = ::std::result::Result; +pub type Result = ::std::result::Result; /// The main crate-wide error type. #[derive(Debug, Fail)] -pub enum IrcError { +pub enum Error { /// An internal I/O error. #[fail(display = "an io error occurred")] Io(#[cause] IoError), @@ -35,26 +34,18 @@ pub enum IrcError { #[fail(display = "a TLS error occurred")] Tls(#[cause] TlsError), - /// An error caused by Tokio being unable to spawn a task. - #[fail(display = "unable to spawn task")] - Spawn(#[cause] SpawnError), - /// An internal synchronous channel closed. #[fail(display = "a sync channel closed")] SyncChannelClosed(#[cause] RecvError), /// An internal asynchronous channel closed. #[fail(display = "an async channel closed")] - AsyncChannelClosed(#[cause] SendError), + AsyncChannelClosed(#[cause] SendError), /// An internal oneshot channel closed. #[fail(display = "a oneshot channel closed")] OneShotCanceled(#[cause] Canceled), - /// An internal timer error. - #[fail(display = "timer failed")] - Timer(#[cause] TimerError), - /// Error for invalid configurations. #[fail(display = "invalid config: {}", path)] InvalidConfig { @@ -103,13 +94,17 @@ pub enum IrcError { #[fail(display = "none of the specified nicknames were usable")] NoUsableNick, + /// Stream has already been configured. + #[fail(display = "stream has already been configured")] + StreamAlreadyConfigured, + /// This allows you to produce any `failure::Error` within closures used by /// the irc crate. No errors of this kind will ever be produced by the crate /// itself. #[fail(display = "{}", inner)] Custom { /// The actual error that occurred. - inner: failure::Error + inner: failure::Error, }, } @@ -170,55 +165,49 @@ pub enum TomlError { Write(#[cause] TomlWriteError), } -impl From for IrcError { - fn from(e: ProtocolError) -> IrcError { +impl From for Error { + fn from(e: ProtocolError) -> Error { match e { - ProtocolError::Io(e) => IrcError::Io(e), - ProtocolError::InvalidMessage { string, cause } => IrcError::InvalidMessage { - string, cause - }, + ProtocolError::Io(e) => Error::Io(e), + ProtocolError::InvalidMessage { string, cause } => { + Error::InvalidMessage { string, cause } + } } } } -impl From for IrcError { - fn from(e: IoError) -> IrcError { - IrcError::Io(e) +impl From for Error { + fn from(e: IoError) -> Error { + Error::Io(e) } } -impl From for IrcError { - fn from(e: TlsError) -> IrcError { - IrcError::Tls(e) +impl From for Error { + fn from(e: TlsError) -> Error { + Error::Tls(e) } } -impl From for IrcError { - fn from(e: SpawnError) -> IrcError { - IrcError::Spawn(e) +impl From for Error { + fn from(e: RecvError) -> Error { + Error::SyncChannelClosed(e) } } -impl From for IrcError { - fn from(e: RecvError) -> IrcError { - IrcError::SyncChannelClosed(e) +impl From for Error { + fn from(e: SendError) -> Error { + Error::AsyncChannelClosed(e) } } -impl From> for IrcError { - fn from(e: SendError) -> IrcError { - IrcError::AsyncChannelClosed(e) +impl From> for Error { + fn from(e: TrySendError) -> Error { + Error::AsyncChannelClosed(e.into_send_error()) } } -impl From for IrcError { - fn from(e: Canceled) -> IrcError { - IrcError::OneShotCanceled(e) - } -} - -impl From for IrcError { - fn from(e: TimerError) -> IrcError { - IrcError::Timer(e) +impl From for Error { + fn from(e: Canceled) -> Error { + Error::OneShotCanceled(e) } } diff --git a/src/lib.rs b/src/lib.rs index 813d0d7..a3b1ce1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,56 +17,36 @@ //! # Example //! //! ```no_run -//! # extern crate irc; //! use irc::client::prelude::*; +//! use futures::prelude::*; //! -//! # fn main() { +//! # #[tokio::main] +//! # async fn main() -> irc::error::Result<()> { //! // configuration is loaded from config.toml into a Config -//! let client = IrcClient::new("config.toml").unwrap(); +//! let mut client = Client::new("config.toml").await?; //! // identify comes from ClientExt -//! client.identify().unwrap(); -//! // for_each_incoming comes from Client -//! client.for_each_incoming(|irc_msg| { -//! // irc_msg is a Message -//! if let Command::PRIVMSG(channel, message) = irc_msg.command { +//! client.identify()?; +//! +//! let mut stream = client.stream()?; +//! +//! while let Some(message) = stream.next().await.transpose()? { +//! if let Command::PRIVMSG(channel, message) = message.command { //! if message.contains(&*client.current_nickname()) { //! // send_privmsg comes from ClientExt //! client.send_privmsg(&channel, "beep boop").unwrap(); //! } //! } -//! }).unwrap(); +//! } +//! # Ok(()) //! # } //! ``` #![warn(missing_docs)] -extern crate bufstream; -extern crate bytes; -extern crate chrono; #[macro_use] extern crate failure; -extern crate encoding; -#[macro_use] -extern crate futures; + pub extern crate irc_proto as proto; -#[macro_use] -extern crate log; -extern crate native_tls; -extern crate serde; -#[macro_use] -extern crate serde_derive; -#[cfg(feature = "json")] -extern crate serde_json; -#[cfg(feature = "yaml")] -extern crate serde_yaml; -extern crate tokio; -extern crate tokio_codec; -extern crate tokio_io; -extern crate tokio_mockstream; -extern crate tokio_timer; -extern crate tokio_tls; -#[cfg(feature = "toml")] -extern crate toml; pub mod client; pub mod error;