Merge pull request #184 from udoprog/tokio-bump

First stab at bumping the project to tokio 0.2
This commit is contained in:
Aaron Weiss 2019-12-27 10:07:00 -05:00 committed by GitHub
commit cebd250f00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2361 additions and 2767 deletions

3
.gitignore vendored
View file

@ -2,7 +2,4 @@
/irc-proto/target /irc-proto/target
/Cargo.lock /Cargo.lock
/irc-proto/Cargo.lock /irc-proto/Cargo.lock
*.json
*.toml
*.yaml
!Cargo.toml !Cargo.toml

View file

@ -2,17 +2,11 @@ language: rust
rust: stable rust: stable
sudo: false sudo: false
script: script:
- chmod +x mktestconfig.sh - cargo test --all --features "toml yaml json"
- ./mktestconfig.sh - cargo build
- pushd irc-proto # No idea how to fix this, since we don't depend on futures-preview directly.
- cargo test --verbose # - rustdoc --test README.md --extern irc=target/debug/libirc.rlib -L target/debug/deps --edition 2018
- popd - cargo run --example build-bot
- 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
notifications: notifications:
email: false email: false
irc: irc:

View file

@ -9,6 +9,7 @@ categories = ["asynchronous", "network-programming"]
documentation = "https://docs.rs/irc/" documentation = "https://docs.rs/irc/"
repository = "https://github.com/aatxe/irc" repository = "https://github.com/aatxe/irc"
readme = "README.md" readme = "README.md"
edition = "2018"
[badges] [badges]
travis-ci = { repository = "aatxe/irc" } travis-ci = { repository = "aatxe/irc" }
@ -31,22 +32,25 @@ bytes = "0.4"
chrono = "0.4" chrono = "0.4"
encoding = "0.2" encoding = "0.2"
failure = "0.1" failure = "0.1"
futures = "0.1"
irc-proto = { version = "*", path = "irc-proto" } irc-proto = { version = "*", path = "irc-proto" }
log = "0.4" log = "0.4"
native-tls = "0.2" native-tls = "0.2"
serde = "1.0" serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0" 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_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.7", 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 } 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] [dev-dependencies]
futures = "0.3.1"
anyhow = "1.0.13"
args = "2.0" args = "2.0"
getopts = "0.2" getopts = "0.2"
env_logger = "0.6.2"

View file

@ -48,21 +48,16 @@ documentation, and below.
[irc-prelude]: https://docs.rs/irc/*/irc/client/prelude/index.html [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.14 replaced all existing APIs with one based on async/await.
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;
```rust,no_run,edition2018
use irc::client::prelude::*; 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") // We can also load the Config at runtime via Config::load("path/to/config.toml")
let config = Config { let config = Config {
nickname: Some("the-irc-crate".to_owned()), nickname: Some("the-irc-crate".to_owned()),
@ -71,50 +66,16 @@ fn main() {
..Config::default() ..Config::default()
}; };
let mut reactor = IrcReactor::new().unwrap(); let mut client = Client::from_config(config).await?;
let client = reactor.prepare_client_and_connect(config).unwrap(); client.identify()?;
client.identify().unwrap();
reactor.register_client_with_handler(client, |client, message| { let mut stream = client.stream()?;
while let Some(message) = stream.next().await.transpose()? {
print!("{}", message); print!("{}", message);
// And here we can do whatever we want with the messages. }
Ok(())
});
reactor.run().unwrap(); Ok(())
}
```
### 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()
} }
``` ```

View file

@ -1,10 +1,12 @@
extern crate irc; extern crate irc;
use futures::prelude::*;
use irc::client::prelude::*; use irc::client::prelude::*;
use std::default::Default; use std::default::Default;
use std::env; use std::env;
fn main() { #[tokio::main]
async fn main() -> irc::error::Result<()> {
let repository_slug = env::var("TRAVIS_REPO_SLUG").unwrap(); let repository_slug = env::var("TRAVIS_REPO_SLUG").unwrap();
let branch = env::var("TRAVIS_BRANCH").unwrap(); let branch = env::var("TRAVIS_BRANCH").unwrap();
let commit = env::var("TRAVIS_COMMIT").unwrap(); let commit = env::var("TRAVIS_COMMIT").unwrap();
@ -17,11 +19,13 @@ fn main() {
..Default::default() ..Default::default()
}; };
let mut reactor = IrcReactor::new().unwrap(); let mut client = Client::from_config(config).await?;
let client = reactor.prepare_client_and_connect(config).unwrap();
client.identify().unwrap();
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 { match message.command {
Command::Response(Response::RPL_ISUPPORT, _, _) => { Command::Response(Response::RPL_ISUPPORT, _, _) => {
client.send_privmsg( client.send_privmsg(
@ -34,13 +38,12 @@ fn main() {
commit_message commit_message
), ),
)?; )?;
client.send_quit("QUIT")?; client.send_quit("QUIT")?;
} }
_ => (), _ => (),
}; }
}
Ok(()) Ok(())
});
reactor.run().unwrap();
} }

View file

@ -1,7 +1,3 @@
extern crate args;
extern crate getopts;
extern crate irc;
use std::env; use std::env;
use std::process::exit; use std::process::exit;
@ -34,18 +30,22 @@ fn main() {
fn parse(input: &[String]) -> Result<Option<(String, String)>, ArgsError> { fn parse(input: &[String]) -> Result<Option<(String, String)>, ArgsError> {
let mut args = Args::new(PROGRAM_NAME, PROGRAM_DESC); let mut args = Args::new(PROGRAM_NAME, PROGRAM_DESC);
args.flag("h", "help", "Print the usage menu"); args.flag("h", "help", "Print the usage menu");
args.option("i", args.option(
"input", "i",
"The path to the input config", "input",
"FILE", "The path to the input config",
Occur::Req, "FILE",
None); Occur::Req,
args.option("o", None,
"output", );
"The path to output the new config to", args.option(
"FILE", "o",
Occur::Req, "output",
None); "The path to output the new config to",
"FILE",
Occur::Req,
None,
);
args.parse(input)?; args.parse(input)?;
@ -55,8 +55,5 @@ fn parse(input: &[String]) -> Result<Option<(String, String)>, ArgsError> {
return Ok(None); return Ok(None);
} }
Ok(Some(( Ok(Some((args.value_of("input")?, args.value_of("output")?)))
args.value_of("input")?,
args.value_of("output")?,
)))
} }

View file

@ -1,10 +1,12 @@
extern crate irc; extern crate irc;
use std::default::Default; use futures::prelude::*;
use irc::error; use irc::{client::prelude::*, error};
use irc::client::prelude::*;
#[tokio::main]
async fn main() -> irc::error::Result<()> {
env_logger::init();
fn main() {
let cfg1 = Config { let cfg1 = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.mozilla.org".to_owned()),
@ -20,32 +22,39 @@ fn main() {
}; };
let configs = vec![cfg1, cfg2]; let configs = vec![cfg1, cfg2];
let mut senders = Vec::new();
let mut reactor = IrcReactor::new().unwrap(); let mut streams = Vec::new();
for config in configs { for config in configs {
// Immediate errors like failure to resolve the server's domain or to establish any connection will // 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. // manifest here in the result of prepare_client_and_connect.
let client = reactor.prepare_client_and_connect(config).unwrap(); let mut client = Client::from_config(config).await?;
client.identify().unwrap(); client.identify()?;
// Here, we tell the reactor to setup this client for future handling (in run) using the specified
// handler function process_msg. senders.push(client.sender());
reactor.register_client_with_handler(client, process_msg); streams.push(client.stream()?);
} }
// Runtime errors like a dropped connection will manifest here in the result of run. loop {
reactor.run().unwrap(); 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<()> { fn process_msg(sender: &Sender, message: Message) -> error::Result<()> {
print!("{}", message); // print!("{}", message);
match message.command { match message.command {
Command::PRIVMSG(ref target, ref msg) => { Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") { if msg.contains("pickles") {
client.send_privmsg(target, "Hi!")?; sender.send_privmsg(target, "Hi!")?;
} }
} }
_ => (), _ => (),
} }
Ok(()) Ok(())
} }

View file

@ -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();
}

View file

@ -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(())
}

View file

@ -1,9 +1,8 @@
extern crate irc; use futures::prelude::*;
use std::default::Default;
use irc::client::prelude::*; use irc::client::prelude::*;
fn main() { #[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("repeater".to_owned()), nickname: Some("repeater".to_owned()),
alt_nicks: Some(vec!["blaster".to_owned(), "smg".to_owned()]), alt_nicks: Some(vec!["blaster".to_owned(), "smg".to_owned()]),
@ -15,11 +14,14 @@ fn main() {
..Default::default() ..Default::default()
}; };
let client = IrcClient::from_config(config).unwrap(); let mut client = Client::from_config(config).await?;
client.identify().unwrap(); 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 let Command::PRIVMSG(ref target, ref msg) = message.command {
if msg.starts_with(&*client.current_nickname()) { if msg.starts_with(&*client.current_nickname()) {
let tokens: Vec<_> = msg.split(' ').collect(); let tokens: Vec<_> = msg.split(' ').collect();
@ -29,12 +31,12 @@ fn main() {
for _ in 0..count { for _ in 0..count {
client.send_privmsg( client.send_privmsg(
message.response_target().unwrap_or(target), message.response_target().unwrap_or(target),
&msg[n..] &msg[n..],
).unwrap(); )?;
} }
} }
} }
} }
} }
}).unwrap() }
} }

View file

@ -1,9 +1,8 @@
extern crate irc; use futures::prelude::*;
use std::default::Default;
use irc::client::prelude::*; use irc::client::prelude::*;
fn main() { #[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]), alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]),
@ -12,15 +11,18 @@ fn main() {
..Default::default() ..Default::default()
}; };
let client = IrcClient::from_config(config).unwrap(); let mut client = Client::from_config(config).await?;
client.identify().unwrap(); 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 let Command::PRIVMSG(ref target, ref msg) = message.command {
if msg.contains("pickles") { if msg.contains("pickles") {
client.send_privmsg(target, "Hi!").unwrap(); client.send_privmsg(target, "Hi!").unwrap();
} }
} }
}).unwrap(); }
} }

View file

@ -1,9 +1,8 @@
extern crate irc; use futures::prelude::*;
use std::default::Default;
use irc::client::prelude::*; use irc::client::prelude::*;
fn main() { #[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.mozilla.org".to_owned()),
@ -12,15 +11,17 @@ fn main() {
..Default::default() ..Default::default()
}; };
let client = IrcClient::from_config(config).unwrap(); let mut client = Client::from_config(config).await?;
client.identify().unwrap(); 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 let Command::PRIVMSG(ref target, ref msg) = message.command {
if msg.contains("pickles") { if msg.contains("pickles") {
client.send_privmsg(target, "Hi!").unwrap(); sender.send_privmsg(target, "Hi!")?;
} }
} }
}).unwrap(); }
} }

View file

@ -1,13 +1,10 @@
extern crate irc; use futures::prelude::*;
extern crate tokio_timer;
use std::default::Default;
use std::time::Duration;
use irc::client::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`. // 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 { let config = Config {
nickname: Some("mastodon".to_owned()), nickname: Some("mastodon".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.mozilla.org".to_owned()),
@ -15,37 +12,13 @@ fn main() {
..Default::default() ..Default::default()
}; };
// We need to create a reactor first and foremost let client = Client::from_config(config).await?;
let mut reactor = IrcReactor::new().unwrap(); let sender = client.sender();
// 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();
// Rather than spawn a thread that reads the messages separately, we register a handler with the let mut interval = tokio::time::interval(Duration::from_secs(1)).fuse();
// 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(())
});
// We construct an interval using a wheel timer from tokio_timer. This interval will fire every loop {
// ten seconds (and is roughly accurate to the second). let _ = interval.select_next_some().await;
let send_interval = tokio_timer::wheel() sender.send_privmsg("#rust-spam", "AWOOOOOOOOOO")?;
.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();
} }

View file

@ -1,27 +1,31 @@
extern crate irc; use futures::prelude::*;
use std::default::Default;
use std::thread;
use std::time::Duration;
use irc::client::prelude::*; use irc::client::prelude::*;
use std::time::Duration;
// NOTE: you can find an asynchronous version of this example with `IrcReactor` in `tooter.rs`. // 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 { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.mozilla.org".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#rust-spam".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default() ..Default::default()
}; };
let client = IrcClient::from_config(config).unwrap();
client.identify().unwrap(); let mut client = Client::from_config(config).await?;
let client2 = client.clone(); client.identify()?;
// Let's set up a loop that just prints the messages.
thread::spawn(move || { let mut stream = client.stream()?;
client2.stream().map(|m| print!("{}", m)).wait().count(); let mut interval = tokio::time::interval(Duration::from_secs(10)).fuse();
});
loop { loop {
client.send_privmsg("#rust-spam", "TWEET TWEET").unwrap(); futures::select! {
thread::sleep(Duration::new(10, 0)); m = stream.select_next_some() => {
println!("{}", m?);
}
_ = interval.select_next_some() => {
client.send_privmsg("#rust-spam", "TWEET TWEET")?;
}
}
} }
} }

View file

@ -13,12 +13,11 @@ repository = "https://github.com/aatxe/irc"
travis-ci = { repository = "aatxe/irc" } travis-ci = { repository = "aatxe/irc" }
[features] [features]
default = ["tokio"] default = ["tokio", "tokio-util", "bytes"]
tokio = ["tokio-codec", "tokio-io", "bytes"]
[dependencies] [dependencies]
bytes = { version = "0.4", optional = true } bytes = { version = "0.5", optional = true }
encoding = "0.2" encoding = "0.2"
failure = "0.1" failure = "0.1"
tokio-codec = { version = "0.1", optional = true } tokio-util = { version = "0.2.0", optional = true }
tokio-io = { version = "0.1", optional = true } tokio = { version = "0.2.0", optional = true }

View file

@ -8,10 +8,10 @@ pub trait ChannelExt {
impl<'a> ChannelExt for &'a str { impl<'a> ChannelExt for &'a str {
fn is_channel_name(&self) -> bool { 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('!')
} }
} }

View file

@ -15,13 +15,11 @@ struct Parser {
/// An extension trait giving strings a function to strip IRC colors /// An extension trait giving strings a function to strip IRC colors
pub trait FormattedStringExt<'a> { pub trait FormattedStringExt<'a> {
/// Returns true if the string contains color, bold, underline or italics /// Returns true if the string contains color, bold, underline or italics
fn is_formatted(&self) -> bool; fn is_formatted(&self) -> bool;
/// Returns the string with all color, bold, underline and italics stripped /// Returns the string with all color, bold, underline and italics stripped
fn strip_formatting(self) -> Cow<'a, str>; fn strip_formatting(self) -> Cow<'a, str>;
} }
const FORMAT_CHARACTERS: &[char] = &[ const FORMAT_CHARACTERS: &[char] = &[
@ -54,7 +52,7 @@ fn strip_formatting(buf: &mut String) {
impl Parser { impl Parser {
fn new() -> Self { fn new() -> Self {
Parser { Parser {
state: ParserState::Text, state: ParserState::Text,
} }
} }
@ -66,9 +64,7 @@ impl Parser {
self.state = ColorCode; self.state = ColorCode;
false false
} }
Text => { Text => !FORMAT_CHARACTERS.contains(&cur),
!FORMAT_CHARACTERS.contains(&cur)
}
ColorCode if cur.is_digit(10) => { ColorCode if cur.is_digit(10) => {
self.state = Foreground1(cur); self.state = Foreground1(cur);
false false
@ -122,8 +118,8 @@ impl FormattedStringExt<'static> for String {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::borrow::Cow;
use colors::FormattedStringExt; use colors::FormattedStringExt;
use std::borrow::Cow;
macro_rules! test_formatted_string_ext { macro_rules! test_formatted_string_ext {
{ $( $name:ident ( $($line:tt)* ), )* } => { { $( $name:ident ( $($line:tt)* ), )* } => {

View file

@ -1,8 +1,8 @@
//! Enumeration of all available client commands. //! Enumeration of all available client commands.
use std::str::FromStr; use std::str::FromStr;
use error::MessageParseError;
use chan::ChannelExt; use chan::ChannelExt;
use error::MessageParseError;
use mode::{ChannelMode, Mode, UserMode}; use mode::{ChannelMode, Mode, UserMode};
use response::Response; use response::Response;
@ -170,7 +170,12 @@ pub enum Command {
// IRCv3 support // IRCv3 support
/// CAP [*] COMMAND [*] :[param] /// CAP [*] COMMAND [*] :[param]
CAP(Option<String>, CapSubCommand, Option<String>, Option<String>), CAP(
Option<String>,
CapSubCommand,
Option<String>,
Option<String>,
),
// IRCv3.1 extensions // IRCv3.1 extensions
/// AUTHENTICATE data /// AUTHENTICATE data
@ -184,7 +189,12 @@ pub enum Command {
// IRCv3.2 extensions // IRCv3.2 extensions
/// METADATA target COMMAND [params] :[param] /// METADATA target COMMAND [params] :[param]
METADATA(String, Option<MetadataSubCommand>, Option<Vec<String>>, Option<String>), METADATA(
String,
Option<MetadataSubCommand>,
Option<Vec<String>>,
Option<String>,
),
/// MONITOR command [nicklist] /// MONITOR command [nicklist]
MONITOR(String, Option<String>), MONITOR(String, Option<String>),
/// BATCH (+/-)reference-tag [type [params]] /// 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), Some(suffix) => format!("{}{}{} :{}", cmd, sp, args, suffix),
None => format!("{}{}{}", cmd, sp, args), None => format!("{}{}{}", cmd, sp, args),
} }
} }
impl<'a> From<&'a Command> for String { 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::NICK(ref n) => stringify("NICK", &[], Some(n)),
Command::USER(ref u, ref m, ref r) => stringify("USER", &[u, m, "*"], Some(r)), 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::OPER(ref u, ref p) => stringify("OPER", &[u], Some(p)),
Command::UserMODE(ref u, ref m) => { Command::UserMODE(ref u, ref m) => format!(
format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| { "MODE {}{}",
u,
m.iter().fold(String::new(), |mut acc, mode| {
acc.push_str(" "); acc.push_str(" ");
acc.push_str(&mode.to_string()); acc.push_str(&mode.to_string());
acc acc
})) })
} ),
Command::SERVICE(ref n, ref r, ref d, ref t, ref re, ref i) => { Command::SERVICE(ref n, ref r, ref d, ref t, ref re, ref i) => {
stringify("SERVICE", &[n, r, d, t, re], Some(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::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, Some(ref m)) => stringify("PART", &[c], Some(m)),
Command::PART(ref c, None) => stringify("PART", &[c], None), Command::PART(ref c, None) => stringify("PART", &[c], None),
Command::ChannelMODE(ref u, ref m) => { Command::ChannelMODE(ref u, ref m) => format!(
format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| { "MODE {}{}",
u,
m.iter().fold(String::new(), |mut acc, mode| {
acc.push_str(" "); acc.push_str(" ");
acc.push_str(&mode.to_string()); acc.push_str(&mode.to_string());
acc acc
})) })
} ),
Command::TOPIC(ref c, Some(ref t)) => stringify("TOPIC", &[c], Some(t)), Command::TOPIC(ref c, Some(ref t)) => stringify("TOPIC", &[c], Some(t)),
Command::TOPIC(ref c, None) => stringify("TOPIC", &[c], None), Command::TOPIC(ref c, None) => stringify("TOPIC", &[c], None),
Command::NAMES(Some(ref c), Some(ref t)) => stringify("NAMES", &[c], Some(t)), 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(Some(ref t)) => stringify("USERS", &[], Some(t)),
Command::USERS(None) => stringify("USERS", &[], None), Command::USERS(None) => stringify("USERS", &[], None),
Command::WALLOPS(ref t) => stringify("WALLOPS", &[], Some(t)), Command::WALLOPS(ref t) => stringify("WALLOPS", &[], Some(t)),
Command::USERHOST(ref u) => { Command::USERHOST(ref u) => stringify(
stringify( "USERHOST",
"USERHOST", &u.iter().map(|s| &s[..]).collect::<Vec<_>>(),
&u.iter().map(|s| &s[..]).collect::<Vec<_>>(), None,
None, ),
)
}
Command::ISON(ref u) => { Command::ISON(ref u) => {
stringify("ISON", &u.iter().map(|s| &s[..]).collect::<Vec<_>>(), None) stringify("ISON", &u.iter().map(|s| &s[..]).collect::<Vec<_>>(), None)
} }
@ -369,92 +380,80 @@ impl<'a> From<&'a Command> for String {
stringify("METADATA", &[&t[..], c.to_str()], None) stringify("METADATA", &[&t[..], c.to_str()], None)
} }
Command::METADATA(ref t, Some(ref c), Some(ref a), Some(ref p)) => { Command::METADATA(ref t, Some(ref c), Some(ref a), Some(ref p)) => stringify(
stringify( "METADATA",
"METADATA", &vec![t, &c.to_str().to_owned()]
&vec![t, &c.to_str().to_owned()] .iter()
.iter() .map(|s| &s[..])
.map(|s| &s[..]) .chain(a.iter().map(|s| &s[..]))
.chain(a.iter().map(|s| &s[..])) .collect::<Vec<_>>(),
.collect::<Vec<_>>(), Some(p),
Some(p), ),
) Command::METADATA(ref t, Some(ref c), Some(ref a), None) => stringify(
} "METADATA",
Command::METADATA(ref t, Some(ref c), Some(ref a), None) => { &vec![t, &c.to_str().to_owned()]
stringify( .iter()
"METADATA", .map(|s| &s[..])
&vec![t, &c.to_str().to_owned()] .chain(a.iter().map(|s| &s[..]))
.iter() .collect::<Vec<_>>(),
.map(|s| &s[..]) None,
.chain(a.iter().map(|s| &s[..])) ),
.collect::<Vec<_>>(),
None,
)
}
Command::METADATA(ref t, None, None, Some(ref p)) => { Command::METADATA(ref t, None, None, Some(ref p)) => {
stringify("METADATA", &[t], Some(p)) stringify("METADATA", &[t], Some(p))
} }
Command::METADATA(ref t, None, None, None) => stringify("METADATA", &[t], None), Command::METADATA(ref t, None, None, None) => stringify("METADATA", &[t], None),
Command::METADATA(ref t, None, Some(ref a), Some(ref p)) => { Command::METADATA(ref t, None, Some(ref a), Some(ref p)) => stringify(
stringify( "METADATA",
"METADATA", &vec![t]
&vec![t] .iter()
.iter() .map(|s| &s[..])
.map(|s| &s[..]) .chain(a.iter().map(|s| &s[..]))
.chain(a.iter().map(|s| &s[..])) .collect::<Vec<_>>(),
.collect::<Vec<_>>(), Some(p),
Some(p), ),
) Command::METADATA(ref t, None, Some(ref a), None) => stringify(
} "METADATA",
Command::METADATA(ref t, None, Some(ref a), None) => { &vec![t]
stringify( .iter()
"METADATA", .map(|s| &s[..])
&vec![t] .chain(a.iter().map(|s| &s[..]))
.iter() .collect::<Vec<_>>(),
.map(|s| &s[..]) None,
.chain(a.iter().map(|s| &s[..])) ),
.collect::<Vec<_>>(),
None,
)
}
Command::MONITOR(ref c, Some(ref t)) => stringify("MONITOR", &[c, t], None), Command::MONITOR(ref c, Some(ref t)) => stringify("MONITOR", &[c, t], None),
Command::MONITOR(ref c, None) => stringify("MONITOR", &[c], None), Command::MONITOR(ref c, None) => stringify("MONITOR", &[c], None),
Command::BATCH(ref t, Some(ref c), Some(ref a)) => { Command::BATCH(ref t, Some(ref c), Some(ref a)) => stringify(
stringify( "BATCH",
"BATCH", &vec![t, &c.to_str().to_owned()]
&vec![t, &c.to_str().to_owned()] .iter()
.iter() .map(|s| &s[..])
.map(|s| &s[..]) .chain(a.iter().map(|s| &s[..]))
.chain(a.iter().map(|s| &s[..])) .collect::<Vec<_>>(),
.collect::<Vec<_>>(), None,
None, ),
)
}
Command::BATCH(ref t, Some(ref c), None) => stringify("BATCH", &[t, c.to_str()], None), Command::BATCH(ref t, Some(ref c), None) => stringify("BATCH", &[t, c.to_str()], None),
Command::BATCH(ref t, None, Some(ref a)) => { Command::BATCH(ref t, None, Some(ref a)) => stringify(
stringify( "BATCH",
"BATCH", &vec![t]
&vec![t] .iter()
.iter() .map(|s| &s[..])
.map(|s| &s[..]) .chain(a.iter().map(|s| &s[..]))
.chain(a.iter().map(|s| &s[..])) .collect::<Vec<_>>(),
.collect::<Vec<_>>(), None,
None, ),
)
}
Command::BATCH(ref t, None, None) => stringify("BATCH", &[t], None), Command::BATCH(ref t, None, None) => stringify("BATCH", &[t], None),
Command::CHGHOST(ref u, ref h) => stringify("CHGHOST", &[u, h], None), Command::CHGHOST(ref u, ref h) => stringify("CHGHOST", &[u, h], None),
Command::Response(ref resp, ref a, Some(ref s)) => { Command::Response(ref resp, ref a, Some(ref s)) => stringify(
stringify(&format!("{:03}", *resp as u16), &format!("{:03}", *resp as u16),
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(), &a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
Some(s)) Some(s),
} ),
Command::Response(ref resp, ref a, None) => { Command::Response(ref resp, ref a, None) => stringify(
stringify(&format!("{:03}", *resp as u16), &format!("{:03}", *resp as u16),
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(), &a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
None) None,
} ),
Command::Raw(ref c, ref a, Some(ref s)) => { Command::Raw(ref c, ref a, Some(ref s)) => {
stringify(c, &a.iter().map(|s| &s[..]).collect::<Vec<_>>(), Some(s)) stringify(c, &a.iter().map(|s| &s[..]).collect::<Vec<_>>(), Some(s))
} }
@ -467,7 +466,11 @@ impl<'a> From<&'a Command> for String {
impl Command { impl Command {
/// Constructs a new Command. /// Constructs a new Command.
pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> Result<Command, MessageParseError> { pub fn new(
cmd: &str,
args: Vec<&str>,
suffix: Option<&str>,
) -> Result<Command, MessageParseError> {
Ok(if cmd.eq_ignore_ascii_case("PASS") { Ok(if cmd.eq_ignore_ascii_case("PASS") {
match suffix { match suffix {
Some(suffix) => { Some(suffix) => {
@ -539,13 +542,15 @@ impl Command {
} else if cmd.eq_ignore_ascii_case("MODE") { } else if cmd.eq_ignore_ascii_case("MODE") {
match suffix { match suffix {
Some(suffix) => raw(cmd, args, Some(suffix)), Some(suffix) => raw(cmd, args, Some(suffix)),
None => if args[0].is_channel_name() { None => {
let arg = args[1..].join(" "); if args[0].is_channel_name() {
Command::ChannelMODE(args[0].to_owned(), Mode::as_channel_modes(&arg)?) let arg = args[1..].join(" ");
} else { Command::ChannelMODE(args[0].to_owned(), Mode::as_channel_modes(&arg)?)
let arg = args[1..].join(" "); } else {
Command::UserMODE(args[0].to_owned(), Mode::as_user_modes(&arg)?) let arg = args[1..].join(" ");
}, Command::UserMODE(args[0].to_owned(), Mode::as_user_modes(&arg)?)
}
}
} }
} else if cmd.eq_ignore_ascii_case("SERVICE") { } else if cmd.eq_ignore_ascii_case("SERVICE") {
match suffix { match suffix {
@ -1432,26 +1437,22 @@ impl Command {
} else if args.len() == 2 { } else if args.len() == 2 {
if let Ok(cmd) = args[0].parse() { if let Ok(cmd) = args[0].parse() {
match suffix { match suffix {
Some(suffix) => { Some(suffix) => Command::CAP(
Command::CAP( None,
None, cmd,
cmd, Some(args[1].to_owned()),
Some(args[1].to_owned()), Some(suffix.to_owned()),
Some(suffix.to_owned()), ),
)
}
None => Command::CAP(None, cmd, Some(args[1].to_owned()), None), None => Command::CAP(None, cmd, Some(args[1].to_owned()), None),
} }
} else if let Ok(cmd) = args[1].parse() { } else if let Ok(cmd) = args[1].parse() {
match suffix { match suffix {
Some(suffix) => { Some(suffix) => Command::CAP(
Command::CAP( Some(args[0].to_owned()),
Some(args[0].to_owned()), cmd,
cmd, None,
None, Some(suffix.to_owned()),
Some(suffix.to_owned()), ),
)
}
None => Command::CAP(Some(args[0].to_owned()), cmd, None, None), None => Command::CAP(Some(args[0].to_owned()), cmd, None, None),
} }
} else { } else {
@ -1460,22 +1461,18 @@ impl Command {
} else if args.len() == 3 { } else if args.len() == 3 {
if let Ok(cmd) = args[1].parse() { if let Ok(cmd) = args[1].parse() {
match suffix { match suffix {
Some(suffix) => { Some(suffix) => Command::CAP(
Command::CAP( Some(args[0].to_owned()),
Some(args[0].to_owned()), cmd,
cmd, Some(args[2].to_owned()),
Some(args[2].to_owned()), Some(suffix.to_owned()),
Some(suffix.to_owned()), ),
) None => Command::CAP(
} Some(args[0].to_owned()),
None => { cmd,
Command::CAP( Some(args[2].to_owned()),
Some(args[0].to_owned()), None,
cmd, ),
Some(args[2].to_owned()),
None,
)
}
} }
} else { } else {
raw(cmd, args, suffix) raw(cmd, args, suffix)
@ -1521,23 +1518,19 @@ impl Command {
if args.len() == 2 { if args.len() == 2 {
match suffix { match suffix {
Some(_) => raw(cmd, args, suffix), Some(_) => raw(cmd, args, suffix),
None => { None => match args[1].parse() {
match args[1].parse() { Ok(c) => Command::METADATA(args[0].to_owned(), Some(c), None, None),
Ok(c) => Command::METADATA(args[0].to_owned(), Some(c), None, None), Err(_) => raw(cmd, args, suffix),
Err(_) => raw(cmd, args, suffix), },
}
}
} }
} else if args.len() > 2 { } else if args.len() > 2 {
match args[1].parse() { match args[1].parse() {
Ok(c) => { Ok(c) => Command::METADATA(
Command::METADATA( args[0].to_owned(),
args[0].to_owned(), Some(c),
Some(c), Some(args.into_iter().skip(1).map(|s| s.to_owned()).collect()),
Some(args.into_iter().skip(1).map(|s| s.to_owned()).collect()), suffix.map(|s| s.to_owned()),
suffix.map(|s| s.to_owned()), ),
)
}
Err(_) => { Err(_) => {
if args.len() == 3 && suffix.is_some() { if args.len() == 3 && suffix.is_some() {
Command::METADATA( Command::METADATA(
@ -1787,15 +1780,19 @@ impl FromStr for BatchSubCommand {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::Message;
use super::Response;
use super::Command; use super::Command;
use super::Response;
use crate::Message;
#[test] #[test]
fn format_response() { fn format_response() {
assert!(String::from(&Command::Response(Response::RPL_WELCOME, assert!(
vec!["foo".into()], String::from(&Command::Response(
None)) == "001 foo"); Response::RPL_WELCOME,
vec!["foo".into()],
None
)) == "001 foo"
);
} }
#[test] #[test]
@ -1809,6 +1806,9 @@ mod test {
#[test] #[test]
fn parse_user_message() { fn parse_user_message() {
let cmd = "USER a 0 * b".parse::<Message>().unwrap().command; let cmd = "USER a 0 * b".parse::<Message>().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
);
} }
} }

View file

@ -57,7 +57,7 @@ pub enum MessageParseError {
cmd: &'static str, cmd: &'static str,
/// The invalid subcommand. /// The invalid subcommand.
sub: String, sub: String,
} },
} }
/// Errors that occur while parsing mode strings. /// Errors that occur while parsing mode strings.

View file

@ -1,6 +1,6 @@
//! Implementation of IRC codec for Tokio. //! Implementation of IRC codec for Tokio.
use bytes::BytesMut; use bytes::BytesMut;
use tokio_codec::{Decoder, Encoder}; use tokio_util::codec::{Decoder, Encoder};
use error; use error;
use line::LineCodec; use line::LineCodec;
@ -31,7 +31,6 @@ impl IrcCodec {
} }
data data
} }
} }
impl Decoder for IrcCodec { impl Decoder for IrcCodec {
@ -39,9 +38,9 @@ impl Decoder for IrcCodec {
type Error = error::ProtocolError; type Error = error::ProtocolError;
fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<Message>> { fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<Message>> {
self.inner.decode(src).and_then(|res| { self.inner
res.map_or(Ok(None), |msg| msg.parse::<Message>().map(Some)) .decode(src)
}) .and_then(|res| res.map_or(Ok(None), |msg| msg.parse::<Message>().map(Some)))
} }
} }
@ -49,7 +48,6 @@ impl Encoder for IrcCodec {
type Item = Message; type Item = Message;
type Error = error::ProtocolError; type Error = error::ProtocolError;
fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> { fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> {
self.inner.encode(IrcCodec::sanitize(msg.to_string()), dst) self.inner.encode(IrcCodec::sanitize(msg.to_string()), dst)
} }

View file

@ -8,9 +8,9 @@ extern crate encoding;
#[macro_use] #[macro_use]
extern crate failure; extern crate failure;
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
extern crate tokio_codec; extern crate tokio;
#[cfg(feature = "tokio")] #[cfg(feature = "tokio-util")]
extern crate tokio_io; extern crate tokio_util;
pub mod caps; pub mod caps;
pub mod chan; pub mod chan;

View file

@ -3,9 +3,9 @@
use std::io; use std::io;
use bytes::BytesMut; use bytes::BytesMut;
use encoding::{DecoderTrap, EncoderTrap, EncodingRef};
use encoding::label::encoding_from_whatwg_label; 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; use error;
@ -19,11 +19,17 @@ impl LineCodec {
/// Creates a new instance of LineCodec from the specified encoding. /// Creates a new instance of LineCodec from the specified encoding.
pub fn new(label: &str) -> error::Result<LineCodec> { pub fn new(label: &str) -> error::Result<LineCodec> {
encoding_from_whatwg_label(label) encoding_from_whatwg_label(label)
.map(|enc| LineCodec { encoding: enc, next_index: 0 }) .map(|enc| LineCodec {
.ok_or_else(|| io::Error::new( encoding: enc,
io::ErrorKind::InvalidInput, next_index: 0,
&format!("Attempted to use unknown codec {}.", label)[..], })
).into()) .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. // Decode the line using the codec's encoding.
match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) { match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) {
Ok(data) => Ok(Some(data)), Ok(data) => Ok(Some(data)),
Err(data) => Err( Err(data) => Err(io::Error::new(
io::Error::new( io::ErrorKind::InvalidInput,
io::ErrorKind::InvalidInput, &format!("Failed to decode {} as {}.", data, self.encoding.name())[..],
&format!("Failed to decode {} as {}.", data, self.encoding.name())[..], )
).into(), .into()),
),
} }
} else { } else {
// Set the search start index to the current length since we know that none of the // 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<()> { fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> {
// Encode the message using the codec's encoding. // Encode the message using the codec's encoding.
let data: error::Result<Vec<u8>> = self.encoding let data: error::Result<Vec<u8>> = self
.encoding
.encode(&msg, EncoderTrap::Replace) .encode(&msg, EncoderTrap::Replace)
.map_err(|data| { .map_err(|data| {
io::Error::new( io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
&format!("Failed to encode {} as {}.", data, self.encoding.name())[..], &format!("Failed to encode {} as {}.", data, self.encoding.name())[..],
).into() )
.into()
}); });
// Write the encoded message to the output buffer. // Write the encoded message to the output buffer.

View file

@ -9,7 +9,6 @@ use error;
use error::{MessageParseError, ProtocolError}; use error::{MessageParseError, ProtocolError};
use prefix::Prefix; use prefix::Prefix;
/// A data structure representing an IRC message according to the protocol specification. It /// 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 /// 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 /// the protocol command. If the command is unknown, it is treated as a special raw command that
@ -85,7 +84,7 @@ impl Message {
// <servername> ::= <host> // <servername> ::= <host>
self.prefix.as_ref().and_then(|p| match p { self.prefix.as_ref().and_then(|p| match p {
Prefix::Nickname(name, _, _) => Some(&name[..]), Prefix::Nickname(name, _, _) => Some(&name[..]),
_ => None _ => None,
}) })
} }
@ -112,7 +111,7 @@ impl Message {
match self.command { match self.command {
Command::PRIVMSG(ref target, _) if target.is_channel_name() => Some(target), Command::PRIVMSG(ref target, _) if target.is_channel_name() => Some(target),
Command::NOTICE(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 { return Err(ProtocolError::InvalidMessage {
string: s.to_owned(), string: s.to_owned(),
cause: MessageParseError::EmptyMessage, cause: MessageParseError::EmptyMessage,
}) });
} }
let mut state = s; let mut state = s;
@ -210,10 +209,13 @@ impl FromStr for Message {
"\n" "\n"
} else { } else {
"" ""
}.len(); }
.len();
let suffix = if state.contains(" :") { 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]); state = state.find(" :").map_or("", |i| &state[..i + 1]);
suffix suffix
} else { } else {
@ -227,16 +229,18 @@ impl FromStr for Message {
cmd cmd
} }
// If there's no arguments but the "command" starts with colon, it's not a command. // 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 { None if state.starts_with(':') => {
string: s.to_owned(), return Err(ProtocolError::InvalidMessage {
cause: MessageParseError::InvalidCommand, string: s.to_owned(),
}), cause: MessageParseError::InvalidCommand,
})
}
// If there's no arguments following the command, the rest of the state is the command. // If there's no arguments following the command, the rest of the state is the command.
None => { None => {
let cmd = state; let cmd = state;
state = ""; state = "";
cmd cmd
}, }
}; };
let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect(); let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect();
@ -272,7 +276,7 @@ pub struct Tag(pub String, pub Option<String>);
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{Message, Tag}; use super::{Message, Tag};
use command::Command::{PRIVMSG, QUIT, Raw}; use command::Command::{Raw, PRIVMSG, QUIT};
#[test] #[test]
fn new() { fn new() {
@ -398,7 +402,7 @@ mod test {
}; };
assert_eq!( assert_eq!(
"@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \ "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
tags!\r\n" tags!\r\n"
.parse::<Message>() .parse::<Message>()
.unwrap(), .unwrap(),
message message
@ -428,8 +432,9 @@ mod test {
#[test] #[test]
fn from_and_to_string() { fn from_and_to_string() {
let message = "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \ let message =
tags!\r\n"; "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
tags!\r\n";
assert_eq!(message.parse::<Message>().unwrap().to_string(), message); assert_eq!(message.parse::<Message>().unwrap().to_string(), message);
} }

View file

@ -1,10 +1,10 @@
//! A module defining an API for IRC user and channel modes. //! A module defining an API for IRC user and channel modes.
use std::fmt; use std::fmt;
use command::Command;
use error::MessageParseError; use error::MessageParseError;
use error::MessageParseError::InvalidModeString; use error::MessageParseError::InvalidModeString;
use error::ModeParseError::*; use error::ModeParseError::*;
use command::Command;
/// A marker trait for different kinds of Modes. /// A marker trait for different kinds of Modes.
pub trait ModeType: fmt::Display + fmt::Debug + Clone + PartialEq { 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 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::UserMode::*; use self::UserMode::*;
write!(f, "{}", match *self { write!(
Away => 'a', f,
Invisible => 'i', "{}",
Wallops => 'w', match *self {
Restricted => 'r', Away => 'a',
Oper => 'o', Invisible => 'i',
LocalOper => 'O', Wallops => 'w',
ServerNotices => 's', Restricted => 'r',
MaskedHost => 'x', Oper => 'o',
Unknown(c) => c, LocalOper => 'O',
}) ServerNotices => 's',
MaskedHost => 'x',
Unknown(c) => c,
}
)
} }
} }
@ -135,8 +139,8 @@ impl ModeType for ChannelMode {
use self::ChannelMode::*; use self::ChannelMode::*;
match *self { match *self {
Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop | Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop
Voice => true, | Voice => true,
_ => false, _ => false,
} }
} }
@ -172,25 +176,29 @@ impl fmt::Display for ChannelMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::ChannelMode::*; use self::ChannelMode::*;
write!(f, "{}", match *self { write!(
Ban => 'b', f,
Exception => 'e', "{}",
Limit => 'l', match *self {
InviteOnly => 'i', Ban => 'b',
InviteException => 'I', Exception => 'e',
Key => 'k', Limit => 'l',
Moderated => 'm', InviteOnly => 'i',
RegisteredOnly => 'r', InviteException => 'I',
Secret => 's', Key => 'k',
ProtectedTopic => 't', Moderated => 'm',
NoExternalMessages => 'n', RegisteredOnly => 'r',
Founder => 'q', Secret => 's',
Admin => 'a', ProtectedTopic => 't',
Oper => 'o', NoExternalMessages => 'n',
Halfop => 'h', Founder => 'q',
Voice => 'v', Admin => 'a',
Unknown(c) => c, Oper => 'o',
}) Halfop => 'h',
Voice => 'v',
Unknown(c) => c,
}
)
} }
} }
@ -257,14 +265,18 @@ impl Mode<UserMode> {
let init = match chars.next() { let init = match chars.next() {
Some('+') => Plus, Some('+') => Plus,
Some('-') => Minus, Some('-') => Minus,
Some(c) => return Err(InvalidModeString { Some(c) => {
string: s.to_owned(), return Err(InvalidModeString {
cause: InvalidModeModifier { modifier: c }, string: s.to_owned(),
}), cause: InvalidModeModifier { modifier: c },
None => return Err(InvalidModeString { })
string: s.to_owned(), }
cause: MissingModeModifier, None => {
}), return Err(InvalidModeString {
string: s.to_owned(),
cause: MissingModeModifier,
})
}
}; };
for c in chars { for c in chars {
@ -303,14 +315,18 @@ impl Mode<ChannelMode> {
let init = match chars.next() { let init = match chars.next() {
Some('+') => Plus, Some('+') => Plus,
Some('-') => Minus, Some('-') => Minus,
Some(c) => return Err(InvalidModeString { Some(c) => {
string: s.to_owned(), return Err(InvalidModeString {
cause: InvalidModeModifier { modifier: c }, string: s.to_owned(),
}), cause: InvalidModeModifier { modifier: c },
None => return Err(InvalidModeString { })
string: s.to_owned(), }
cause: MissingModeModifier, None => {
}), return Err(InvalidModeString {
string: s.to_owned(),
cause: MissingModeModifier,
})
}
}; };
for c in chars { for c in chars {

View file

@ -1,6 +1,6 @@
//! A module providing an enum for a message prefix. //! A module providing an enum for a message prefix.
use std::str::FromStr;
use std::fmt; use std::fmt;
use std::str::FromStr;
/// The Prefix indicates "the true origin of the message", according to the server. /// The Prefix indicates "the true origin of the message", according to the server.
#[derive(Clone, Eq, PartialEq, Debug)] #[derive(Clone, Eq, PartialEq, Debug)]
@ -50,12 +50,12 @@ impl Prefix {
'!' if active == Active::Name => { '!' if active == Active::Name => {
is_server = false; is_server = false;
active = Active::User; active = Active::User;
}, }
'@' if active != Active::Host => { '@' if active != Active::Host => {
is_server = false; is_server = false;
active = Active::Host; active = Active::Host;
}, }
_ => { _ => {
// Push onto the active buffer // Push onto the active buffer
@ -63,7 +63,8 @@ impl Prefix {
Active::Name => &mut name, Active::Name => &mut name,
Active::User => &mut user, Active::User => &mut user,
Active::Host => &mut host, Active::Host => &mut host,
}.push(c) }
.push(c)
} }
} }
} }
@ -109,7 +110,7 @@ impl<'a> From<&'a str> for Prefix {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::Prefix::{self, ServerName, Nickname}; use super::Prefix::{self, Nickname, ServerName};
// Checks that str -> parsed -> Display doesn't lose data // Checks that str -> parsed -> Display doesn't lose data
fn test_parse(s: &str) -> Prefix { fn test_parse(s: &str) -> Prefix {
@ -139,10 +140,7 @@ mod test {
#[test] #[test]
fn parse_host() { fn parse_host() {
assert_eq!( assert_eq!(test_parse("host.tld"), ServerName("host.tld".into()))
test_parse("host.tld"),
ServerName("host.tld".into())
)
} }
#[test] #[test]

View file

@ -52,7 +52,7 @@ make_response! {
RPL_BOUNCE = 10, RPL_BOUNCE = 10,
/// Undefined format. (Source: Modern) /// 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, RPL_NONE = 300,
/// `302 :*1<reply> *( " " <reply> )` (Source: RFC2812) /// `302 :*1<reply> *( " " <reply> )` (Source: RFC2812)
RPL_USERHOST = 302, RPL_USERHOST = 302,

View file

@ -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

View file

@ -1,31 +1,35 @@
//! A module providing IRC connections for use by `IrcServer`s. //! A module providing IRC connections for use by `IrcServer`s.
use std::fs::File; use futures_channel::mpsc::UnboundedSender;
use std::fmt; use futures_util::{sink::Sink, stream::Stream};
use std::io::Read; use native_tls::{Certificate, Identity, TlsConnector};
use std::{
use encoding::EncoderTrap; fmt,
use encoding::label::encoding_from_whatwg_label; fs::File,
use futures::{Async, Poll, Future, Sink, StartSend, Stream}; io::Read,
use native_tls::{Certificate, TlsConnector, Identity}; pin::Pin,
use tokio_codec::Decoder; task::{Context, Poll},
use tokio::net::{TcpStream}; };
use tokio::net::tcp::ConnectFuture; use tokio::net::TcpStream;
use tokio_mockstream::MockStream;
use tokio_tls::{self, TlsStream}; use tokio_tls::{self, TlsStream};
use tokio_util::codec::Decoder;
use error; use crate::{
use client::data::Config; client::{
use client::transport::{IrcTransport, LogView, Logged}; data::Config,
use proto::{IrcCodec, Message}; transport::{LogView, Logged, Transport},
},
error,
proto::{IrcCodec, Message},
};
/// An IRC connection used internally by `IrcServer`. /// An IRC connection used internally by `IrcServer`.
pub enum Connection { pub enum Connection {
#[doc(hidden)] #[doc(hidden)]
Unsecured(IrcTransport<TcpStream>), Unsecured(Transport<TcpStream>),
#[doc(hidden)] #[doc(hidden)]
Secured(IrcTransport<TlsStream<TcpStream>>), Secured(Transport<TlsStream<TcpStream>>),
#[doc(hidden)] #[doc(hidden)]
Mock(Logged<MockStream>), Mock(Logged<crate::client::mock::MockStream>),
} }
impl fmt::Debug for Connection { 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<Future<Error = error::IrcError, Item = TlsStream<TcpStream>> + 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<Self::Item, Self::Error> {
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 { impl Connection {
/// Creates a new `Connection` using the specified `Config` /// Creates a new `Connection` using the specified `Config`
pub fn new(config: Config) -> error::Result<ConnectionFuture> { pub(crate) async fn new(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Connection> {
if config.use_mock_connection() { if config.use_mock_connection() {
Ok(ConnectionFuture::Mock(config)) use encoding::{label::encoding_from_whatwg_label, EncoderTrap};
} else if config.use_ssl() {
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()?); let domain = format!("{}", config.server()?);
info!("Connecting via SSL to {}.", domain); log::info!("Connecting via SSL to {}.", domain);
let mut builder = TlsConnector::builder(); let mut builder = TlsConnector::builder();
if let Some(cert_path) = config.cert_path() { if let Some(cert_path) = config.cert_path() {
let mut file = File::open(cert_path)?; let mut file = File::open(cert_path)?;
let mut cert_data = vec![]; let mut cert_data = vec![];
file.read_to_end(&mut cert_data)?; file.read_to_end(&mut cert_data)?;
let cert = Certificate::from_der(&cert_data)?; let cert = Certificate::from_der(&cert_data)?;
builder.add_root_certificate(cert); 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() { if let Some(client_cert_path) = config.client_cert_path() {
let client_cert_pass = config.client_cert_pass(); let client_cert_pass = config.client_cert_pass();
let mut file = File::open(client_cert_path)?; let mut file = File::open(client_cert_path)?;
@ -145,25 +96,27 @@ impl Connection {
file.read_to_end(&mut client_cert_data)?; file.read_to_end(&mut client_cert_data)?;
let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?; let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?;
builder.identity(pkcs12_archive); 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 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(); let socket = TcpStream::connect(&config.socket_addr()?).await?;
res let stream = connector.connect(&domain, socket).await?;
}).and_then(move |socket| { let framed = IrcCodec::new(config.encoding())?.framed(stream);
connector.connect(&domain, socket).map_err( let transport = Transport::new(&config, framed, tx);
|e| e.into(),
) Ok(Connection::Secured(transport))
}));
Ok(ConnectionFuture::Secured(config, stream))
} else { } else {
info!("Connecting to {}.", config.server()?); log::info!("Connecting to {}.", config.server()?);
let addr = config.socket_addr()?; let addr = config.socket_addr()?;
Ok(ConnectionFuture::Unsecured( let stream = TcpStream::connect(&addr).await?;
config, let framed = IrcCodec::new(config.encoding())?.framed(stream);
TcpStream::connect(&addr), let transport = Transport::new(&config, framed, tx);
))
Ok(Connection::Unsecured(transport))
} }
} }
@ -178,35 +131,49 @@ impl Connection {
} }
impl Stream for Connection { impl Stream for Connection {
type Item = Message; type Item = error::Result<Message>;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match *self { match &mut *self {
Connection::Unsecured(ref mut inner) => inner.poll(), Connection::Unsecured(inner) => Pin::new(inner).poll_next(cx),
Connection::Secured(ref mut inner) => inner.poll(), Connection::Secured(inner) => Pin::new(inner).poll_next(cx),
Connection::Mock(ref mut inner) => inner.poll(), Connection::Mock(inner) => Pin::new(inner).poll_next(cx),
} }
} }
} }
impl Sink for Connection { impl Sink<Message> for Connection {
type SinkItem = Message; type Error = error::Error;
type SinkError = error::IrcError;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> { fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match *self { match &mut *self {
Connection::Unsecured(ref mut inner) => inner.start_send(item), Connection::Unsecured(inner) => Pin::new(inner).poll_ready(cx),
Connection::Secured(ref mut inner) => inner.start_send(item), Connection::Secured(inner) => Pin::new(inner).poll_ready(cx),
Connection::Mock(ref mut inner) => inner.start_send(item), Connection::Mock(inner) => Pin::new(inner).poll_ready(cx),
} }
} }
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
match *self { match &mut *self {
Connection::Unsecured(ref mut inner) => inner.poll_complete(), Connection::Unsecured(inner) => Pin::new(inner).start_send(item),
Connection::Secured(ref mut inner) => inner.poll_complete(), Connection::Secured(inner) => Pin::new(inner).start_send(item),
Connection::Mock(ref mut inner) => inner.poll_complete(), Connection::Mock(inner) => Pin::new(inner).start_send(item),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
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<Result<(), Self::Error>> {
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),
} }
} }
} }

View file

@ -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": {}
}

View file

@ -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]

View file

@ -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: {}

View file

@ -1,10 +1,12 @@
//! JSON configuration files using serde //! JSON configuration files using serde
use std::borrow::ToOwned; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::{
use std::fs::File; collections::HashMap,
use std::io::prelude::*; fs::File,
use std::net::{SocketAddr, ToSocketAddrs}; io::prelude::*,
use std::path::{Path, PathBuf}; net::{SocketAddr, ToSocketAddrs},
path::{Path, PathBuf},
};
#[cfg(feature = "json")] #[cfg(feature = "json")]
use serde_json; use serde_json;
@ -13,10 +15,10 @@ use serde_yaml;
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
use toml; use toml;
use crate::error::Error::InvalidConfig;
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
use error::TomlError; use crate::error::TomlError;
use error::{ConfigError, Result}; use crate::error::{ConfigError, Result};
use error::IrcError::InvalidConfig;
/// Configuration for IRC clients. /// Configuration for IRC clients.
/// ///
@ -153,9 +155,10 @@ impl Config {
} }
fn path(&self) -> String { fn path(&self) -> String {
self.path.as_ref().map(|buf| buf.to_string_lossy().into_owned()).unwrap_or_else(|| { self.path
"<none>".to_owned() .as_ref()
}) .map(|buf| buf.to_string_lossy().into_owned())
.unwrap_or_else(|| "<none>".to_owned())
} }
/// Loads a configuration from the desired path. This will use the file extension to detect /// Loads a configuration from the desired path. This will use the file extension to detect
@ -182,68 +185,54 @@ impl Config {
}), }),
}; };
res.map(|config| { res.map(|config| config.with_path(path))
config.with_path(path)
})
} }
#[cfg(feature = "json")] #[cfg(feature = "json")]
fn load_json<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> { fn load_json<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
serde_json::from_str(&data[..]).map_err(|e| { serde_json::from_str(data).map_err(|e| InvalidConfig {
InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(),
path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidJson(e),
cause: ConfigError::InvalidJson(e),
}
}) })
} }
#[cfg(not(feature = "json"))] #[cfg(not(feature = "json"))]
fn load_json<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> { fn load_json<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig { Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(), path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
format: "JSON"
}
}) })
} }
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn load_toml<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> { fn load_toml<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
toml::from_str(&data[..]).map_err(|e| { toml::from_str(data).map_err(|e| InvalidConfig {
InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(),
path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidToml(TomlError::Read(e)),
cause: ConfigError::InvalidToml(TomlError::Read(e)),
}
}) })
} }
#[cfg(not(feature = "toml"))] #[cfg(not(feature = "toml"))]
fn load_toml<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> { fn load_toml<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig { Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(), path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
format: "TOML"
}
}) })
} }
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
fn load_yaml<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> { fn load_yaml<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
serde_yaml::from_str(&data[..]).map_err(|e| { serde_yaml::from_str(data).map_err(|e| InvalidConfig {
InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(),
path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidYaml(e),
cause: ConfigError::InvalidYaml(e),
}
}) })
} }
#[cfg(not(feature = "yaml"))] #[cfg(not(feature = "yaml"))]
fn load_yaml<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> { fn load_yaml<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig { Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(), path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
format: "YAML"
}
}) })
} }
@ -257,16 +246,20 @@ impl Config {
Some("json") => self.save_json(&path)?, Some("json") => self.save_json(&path)?,
Some("toml") => self.save_toml(&path)?, Some("toml") => self.save_toml(&path)?,
Some("yaml") | Some("yml") => self.save_yaml(&path)?, Some("yaml") | Some("yml") => self.save_yaml(&path)?,
Some(ext) => return Err(InvalidConfig { Some(ext) => {
path: path.as_ref().to_string_lossy().into_owned(), return Err(InvalidConfig {
cause: ConfigError::UnknownConfigFormat { path: path.as_ref().to_string_lossy().into_owned(),
format: ext.to_owned(), cause: ConfigError::UnknownConfigFormat {
}, format: ext.to_owned(),
}), },
None => return Err(InvalidConfig { })
path: path.as_ref().to_string_lossy().into_owned(), }
cause: ConfigError::MissingExtension, None => {
}), return Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::MissingExtension,
})
}
}; };
file.write_all(data.as_bytes())?; file.write_all(data.as_bytes())?;
self.path = Some(path.as_ref().to_owned()); self.path = Some(path.as_ref().to_owned());
@ -275,11 +268,9 @@ impl Config {
#[cfg(feature = "json")] #[cfg(feature = "json")]
fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> { fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
serde_json::to_string(self).map_err(|e| { serde_json::to_string(self).map_err(|e| InvalidConfig {
InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(),
path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidJson(e),
cause: ConfigError::InvalidJson(e),
}
}) })
} }
@ -287,19 +278,15 @@ impl Config {
fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> { fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig { Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(), path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
format: "JSON"
}
}) })
} }
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> { fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
toml::to_string(self).map_err(|e| { toml::to_string(self).map_err(|e| InvalidConfig {
InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(),
path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidToml(TomlError::Write(e)),
cause: ConfigError::InvalidToml(TomlError::Write(e)),
}
}) })
} }
@ -307,19 +294,15 @@ impl Config {
fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> { fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig { Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(), path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
format: "TOML"
}
}) })
} }
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> { fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
serde_yaml::to_string(self).map_err(|e| { serde_yaml::to_string(self).map_err(|e| InvalidConfig {
InvalidConfig { path: path.as_ref().to_string_lossy().into_owned(),
path: path.as_ref().to_string_lossy().into_owned(), cause: ConfigError::InvalidYaml(e),
cause: ConfigError::InvalidYaml(e),
}
}) })
} }
@ -327,9 +310,7 @@ impl Config {
fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> { fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig { Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(), path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
format: "YAML"
}
}) })
} }
@ -343,12 +324,13 @@ impl Config {
/// Gets the nickname specified in the configuration. /// Gets the nickname specified in the configuration.
pub fn nickname(&self) -> Result<&str> { pub fn nickname(&self) -> Result<&str> {
self.nickname.as_ref().map(|s| &s[..]).ok_or_else(|| { self.nickname
InvalidConfig { .as_ref()
.map(|s| &s[..])
.ok_or_else(|| InvalidConfig {
path: self.path(), path: self.path(),
cause: ConfigError::NicknameNotSpecified, cause: ConfigError::NicknameNotSpecified,
} })
})
} }
/// Gets the bot's nickserv password specified in the configuration. /// Gets the bot's nickserv password specified in the configuration.
@ -360,48 +342,52 @@ impl Config {
/// Gets the alternate nicknames specified in the configuration. /// Gets the alternate nicknames specified in the configuration.
/// This defaults to an empty vector when not specified. /// This defaults to an empty vector when not specified.
pub fn alternate_nicknames(&self) -> Vec<&str> { pub fn alternate_nicknames(&self) -> Vec<&str> {
self.alt_nicks.as_ref().map_or(vec![], |v| { self.alt_nicks
v.iter().map(|s| &s[..]).collect() .as_ref()
}) .map_or(vec![], |v| v.iter().map(|s| &s[..]).collect())
} }
/// Gets the username specified in the configuration. /// Gets the username specified in the configuration.
/// This defaults to the user's nickname when not specified. /// This defaults to the user's nickname when not specified.
pub fn username(&self) -> &str { 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. /// Gets the real name specified in the configuration.
/// This defaults to the user's nickname when not specified. /// This defaults to the user's nickname when not specified.
pub fn real_name(&self) -> &str { 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. /// Gets the address of the server specified in the configuration.
pub fn server(&self) -> Result<&str> { pub fn server(&self) -> Result<&str> {
self.server.as_ref().map(|s| &s[..]).ok_or_else(|| { self.server
InvalidConfig { .as_ref()
.map(|s| &s[..])
.ok_or_else(|| InvalidConfig {
path: self.path(), path: self.path(),
cause: ConfigError::ServerNotSpecified, cause: ConfigError::ServerNotSpecified,
} })
})
} }
/// Gets the port of the server specified in the configuration. /// 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. /// This defaults to 6667 (or 6697 if use_ssl is specified as true) when not specified.
pub fn port(&self) -> u16 { pub fn port(&self) -> u16 {
self.port.as_ref().cloned().unwrap_or(if self.use_ssl() { self.port
6697 .as_ref()
} else { .cloned()
6667 .unwrap_or(if self.use_ssl() { 6697 } else { 6667 })
})
} }
/// Gets the server and port as a `SocketAddr`. /// Gets the server and port as a `SocketAddr`.
/// This panics when server is not specified or the address is malformed. /// This panics when server is not specified or the address is malformed.
pub fn socket_addr(&self) -> Result<SocketAddr> { pub fn socket_addr(&self) -> Result<SocketAddr> {
format!("{}:{}", self.server()?, self.port()).to_socket_addrs() format!("{}:{}", self.server()?, self.port())
.to_socket_addrs()
.map(|mut i| i.next().unwrap()) .map(|mut i| i.next().unwrap())
.map_err(|e| e.into()) .map_err(|e| e.into())
} }
@ -442,17 +428,16 @@ impl Config {
/// Gets the channels to join upon connection. /// Gets the channels to join upon connection.
/// This defaults to an empty vector if it's not specified. /// This defaults to an empty vector if it's not specified.
pub fn channels(&self) -> Vec<&str> { pub fn channels(&self) -> Vec<&str> {
self.channels.as_ref().map_or(vec![], |v| { self.channels
v.iter().map(|s| &s[..]).collect() .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. /// Gets the key for the specified channel if it exists in the configuration.
pub fn channel_key(&self, chan: &str) -> Option<&str> { pub fn channel_key(&self, chan: &str) -> Option<&str> {
self.channel_keys.as_ref().and_then(|m| { self.channel_keys
m.get(&chan.to_owned()).map(|s| &s[..]) .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. /// 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. /// This defaults to `irc:version:env` when not specified.
/// For example, `irc:0.12.0:Compiled with rustc` /// For example, `irc:0.12.0:Compiled with rustc`
pub fn version(&self) -> &str { 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. /// Gets the string to be sent in response to CTCP SOURCE requests.
/// This defaults to `https://github.com/aatxe/irc` when not specified. /// This defaults to `https://github.com/aatxe/irc` when not specified.
pub fn source(&self) -> &str { pub fn source(&self) -> &str {
self.source.as_ref().map_or( self.source
"https://github.com/aatxe/irc", .as_ref()
|s| &s[..], .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. /// 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. /// Gets the NickServ command sequence to recover a nickname.
/// This defaults to `["GHOST"]` when not specified. /// This defaults to `["GHOST"]` when not specified.
pub fn ghost_sequence(&self) -> Vec<&str> { pub fn ghost_sequence(&self) -> Vec<&str> {
self.ghost_sequence.as_ref().map_or(vec!["GHOST"], |v| { self.ghost_sequence
v.iter().map(|s| &s[..]).collect() .as_ref()
}) .map_or(vec!["GHOST"], |v| v.iter().map(|s| &s[..]).collect())
} }
/// Looks up the specified string in the options map. /// Looks up the specified string in the options map.
pub fn get_option(&self, option: &str) -> Option<&str> { pub fn get_option(&self, option: &str) -> Option<&str> {
self.options.as_ref().and_then(|o| { self.options
o.get(&option.to_owned()).map(|s| &s[..]) .as_ref()
}) .and_then(|o| o.get(&option.to_owned()).map(|s| &s[..]))
} }
/// Gets whether or not to use a mock connection for testing. /// Gets whether or not to use a mock connection for testing.
@ -550,12 +534,8 @@ impl Config {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::collections::HashMap;
use std::default::Default;
#[cfg(feature = "json")]
use std::path::Path;
use super::Config; use super::Config;
use std::collections::HashMap;
#[allow(unused)] #[allow(unused)]
fn test_config() -> Config { 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] #[test]
fn is_owner() { fn is_owner() {
let cfg = Config { let cfg = Config {
@ -636,4 +598,37 @@ mod test {
assert_eq!(cfg.get_option("testing"), Some("test")); assert_eq!(cfg.get_option("testing"), Some("test"));
assert_eq!(cfg.get_option("not"), None); 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(())
}
} }

View file

@ -1,7 +1,7 @@
//! Data related to IRC functionality. //! Data related to IRC functionality.
pub use client::data::config::Config; pub use crate::client::data::config::Config;
pub use client::data::user::{AccessLevel, User}; pub use crate::client::data::user::{AccessLevel, User};
pub mod config; pub mod config;
pub mod user; pub mod user;

View file

@ -1,10 +1,10 @@
//! Data for tracking user information. //! Data for tracking user information.
use std::borrow::ToOwned; use std::borrow::ToOwned;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::cmp::Ordering::{Less, Equal, Greater}; use std::cmp::Ordering::{Equal, Greater, Less};
use std::str::FromStr; use std::str::FromStr;
use proto::{Mode, ChannelMode}; use crate::proto::{ChannelMode, Mode};
/// IRC User data. /// IRC User data.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -124,8 +124,9 @@ impl User {
impl PartialEq for User { impl PartialEq for User {
fn eq(&self, other: &User) -> bool { fn eq(&self, other: &User) -> bool {
self.nickname == other.nickname && self.username == other.username && self.nickname == other.nickname
self.hostname == other.hostname && self.username == other.username
&& self.hostname == other.hostname
} }
} }
@ -208,7 +209,9 @@ struct AccessLevelIterator {
impl AccessLevelIterator { impl AccessLevelIterator {
pub fn new(value: &str) -> 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)] #[cfg(test)]
mod test { mod test {
use super::{AccessLevel, User};
use super::AccessLevel::*; use super::AccessLevel::*;
use proto::ChannelMode as M; use super::{AccessLevel, User};
use proto::Mode::*; use crate::proto::ChannelMode as M;
use crate::proto::Mode::*;
#[test] #[test]
fn parse_access_level() { fn parse_access_level() {
@ -269,7 +272,6 @@ mod test {
assert_eq!(user, exp); assert_eq!(user, exp);
assert_eq!(user.highest_access_level, exp.highest_access_level); assert_eq!(user.highest_access_level, exp.highest_access_level);
assert_eq!(user.access_levels, exp.access_levels); assert_eq!(user.access_levels, exp.access_levels);
} }
#[test] #[test]

View file

@ -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<S: ToString>(&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<S>(&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<S>(&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<S1, S2>(&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<S>(&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<S1, S2>(&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<S1, S2>(&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<S1, S2>(&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<S1, S2>(&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<S1, S2>(&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<S1, S2, S3>(&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<S, T>(&self, target: S, modes: &[Mode<T>]) -> 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<S1, S2, S3>(&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<S1, S2>(&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<S1, S2>(&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<S>(&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<S1, S2>(&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<S1, S2>(&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<S: ToString>(&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<S>(&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<S>(&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<S>(&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<S>(&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<S>(&self, target: S) -> Result<()>
where
Self: Sized,
S: ToString,
{
self.send_ctcp(target, "TIME")
}
}
impl<C> 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"
);
}
}

66
src/client/mock.rs Normal file
View file

@ -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<Vec<u8>>,
received: Cursor<Vec<u8>>,
}
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<io::Result<usize>> {
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<Result<usize, io::Error>> {
Poll::Ready(self.as_mut().written.write(buf))
}
fn poll_flush(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
Poll::Ready(self.as_mut().written.flush())
}
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
Poll::Ready(Ok(()))
}
}

File diff suppressed because it is too large Load diff

View file

@ -21,11 +21,10 @@
//! dealing with IRC channel and user modes. They appear in methods for sending mode commands, //! 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. //! as well as in the parsed form of received mode commands.
pub use client::data::Config; pub use crate::{
pub use client::reactor::IrcReactor; client::{data::Config, Client, Sender},
pub use client::{EachIncomingExt, IrcClient, Client}; proto::{
pub use client::ext::ClientExt; Capability, ChannelExt, ChannelMode, Command, Message, Mode, NegotiationVersion, Prefix,
pub use proto::{Capability, ChannelExt, Command, Message, Prefix, NegotiationVersion, Response}; Response, UserMode,
pub use proto::{ChannelMode, Mode, UserMode}; },
};
pub use futures::{Future, Stream};

View file

@ -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<Box<Future<Item = (), Error = error::IrcError>>>,
}
impl IrcReactor {
/// Creates a new reactor.
pub fn new() -> error::Result<IrcReactor> {
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<IrcClientFuture> {
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<IrcClient> {
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<IrcClient> {
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<F, U>(
&mut self, client: IrcClient, mut handler: F
) where F: FnMut(&IrcClient, Message) -> U + 'static,
U: IntoFuture<Item = (), Error = error::IrcError> + '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<F>(
&mut self, future: F
) where F: IntoFuture<Item = (), Error = error::IrcError> + '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(|_| ()))
}
}

View file

@ -1,241 +1,208 @@
//! An IRC transport that wraps an IRC-framed stream to provide a number of features including //! 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 //! 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. //! as the basis for implementing a more full IRC client.
use std::collections::VecDeque; use std::{
use std::sync::{Arc, RwLock, RwLockReadGuard}; pin::Pin,
use std::time::{Duration, Instant}; sync::{Arc, RwLock, RwLockReadGuard},
task::{Context, Poll},
time::Duration,
};
use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream};
use chrono::prelude::*; use chrono::prelude::*;
use tokio_codec::Framed; use futures_channel::mpsc::UnboundedSender;
use tokio_io::{AsyncRead, AsyncWrite}; use futures_util::{future::Future, ready, sink::Sink, stream::Stream};
use tokio_timer; use tokio::{
use tokio_timer::{Interval, Sleep, Timer}; io::{AsyncRead, AsyncWrite},
time::{self, Delay, Interval},
};
use tokio_util::codec::Framed;
use error; use crate::{
use client::data::Config; client::data::Config,
use proto::{Command, IrcCodec, Message}; error,
proto::{Command, IrcCodec, Message},
};
/// An IRC transport that handles core functionality for the IRC protocol. This is used in the /// Pinger-based futures helper.
/// implementation of `Connection` and ultimately `IrcServer`, and plays an important role in struct Pinger {
/// handling connection timeouts, message throttling, and ping response. tx: UnboundedSender<Message>,
pub struct IrcTransport<T>
where
T: AsyncRead + AsyncWrite,
{
/// The inner connection framed with an `IrcCodec`.
inner: Framed<T, IrcCodec>,
/// 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<Sleep>,
/// 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,
/// The amount of time to wait before timing out from no ping response. /// The amount of time to wait before timing out from no ping response.
ping_timeout: u64, ping_timeout: Duration,
/// The last data sent with a ping.
last_ping_data: String,
/// The instant that the last ping was sent to the server. /// The instant that the last ping was sent to the server.
last_ping_sent: Instant, ping_deadline: Option<Delay>,
/// The instant that the last pong was received from the server. /// The interval at which to send pings.
last_pong_received: Instant, ping_interval: Interval,
} }
impl<T> IrcTransport<T> impl Pinger {
where /// Construct a new pinger helper.
T: AsyncRead + AsyncWrite, pub fn new(tx: UnboundedSender<Message>, config: &Config) -> Pinger {
{ let ping_timeout = Duration::from_secs(u64::from(config.ping_timeout()));
/// Creates a new `IrcTransport` from the given IRC stream.
pub fn new(config: &Config, inner: Framed<T, IrcCodec>) -> IrcTransport<T> { Self {
let timer = tokio_timer::wheel().build(); tx,
IrcTransport { ping_timeout,
inner, ping_deadline: None,
burst_timer: tokio_timer::wheel().build(), ping_interval: time::interval(ping_timeout / 2),
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(),
} }
} }
/// Gets the inner stream underlying the `IrcTransport`. /// Handle an incoming message.
pub fn into_inner(self) -> Framed<T, IrcCodec> { fn handle_message(&mut self, message: &Message) -> error::Result<()> {
self.inner 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. /// Send a pong.
fn ping_timed_out(&self) -> bool { fn send_pong(&mut self, data: &str) -> error::Result<()> {
if self.last_pong_received < self.last_ping_sent { self.tx
self.last_ping_sent.elapsed().as_secs() >= self.ping_timeout .unbounded_send(Command::PONG(data.to_owned(), None).into())?;
} else { Ok(())
false
}
} }
/// Sends a ping via the transport. /// Sends a ping via the transport.
fn send_ping(&mut self) -> error::Result<()> { fn send_ping(&mut self) -> error::Result<()> {
log::trace!("Sending PING");
// Creates new ping data using the local timestamp. // Creates new ping data using the local timestamp.
let last_ping_data = format!("{}", Local::now().timestamp()); let data = format!("{}", Local::now().timestamp());
let data = last_ping_data.clone();
let result = self.start_send(Command::PING(data, None).into())?; self.tx
if let AsyncSink::Ready = result { .unbounded_send(Command::PING(data.clone(), None).into())?;
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;
}
Ok(()) Ok(())
} }
/// Polls the most recent burst window from the queue, always returning `NotReady` if none are /// Set the ping deadline.
/// left for whatever reason. fn set_deadline(&mut self) {
fn rolling_burst_window_front(&mut self) -> Result<Async<()>, tokio_timer::TimerError> { if self.ping_deadline.is_none() {
self.rolling_burst_window.front_mut().map(|w| w.poll()).unwrap_or(Ok(Async::NotReady)) let ping_deadline = time::delay_for(self.ping_timeout);
} self.ping_deadline = Some(ping_deadline);
}
impl<T> Stream for IrcTransport<T>
where
T: AsyncRead + AsyncWrite,
{
type Item = Message;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, 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)))
}
} }
} }
} }
impl<T> Sink for IrcTransport<T> impl Future for Pinger {
where type Output = Result<(), error::Error>;
T: AsyncRead + AsyncWrite,
{
type SinkItem = Message;
type SinkError = error::IrcError;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// If the ping timeout has been reached, we close the connection out and return an error. if let Some(ping_deadline) = self.as_mut().ping_deadline.as_mut() {
if self.ping_timed_out() { match Pin::new(ping_deadline).poll(cx) {
self.close()?; Poll::Ready(()) => return Poll::Ready(Err(error::Error::PingTimeout)),
return Err(error::IrcError::PingTimeout) Poll::Pending => (),
}
// 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_complete(&mut self) -> Poll<(), Self::SinkError> { if let Poll::Ready(_) = Pin::new(&mut self.as_mut().ping_interval).poll_next(cx) {
// If the ping timeout has been reached, we close the connection out and return an error. self.as_mut().send_ping()?;
if self.ping_timed_out() { self.as_mut().set_deadline();
self.close()?;
return Err(error::IrcError::PingTimeout)
} }
// If it's time to send a ping, we should do it! This is necessary to ensure that the Poll::Pending
// 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()?;
}
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<T> {
/// The inner connection framed with an `IrcCodec`.
inner: Framed<T, IrcCodec>,
/// Helper for handle pinging.
pinger: Option<Pinger>,
}
impl<T> Unpin for Transport<T> where T: Unpin {}
impl<T> Transport<T>
where
T: Unpin + AsyncRead + AsyncWrite,
{
/// Creates a new `Transport` from the given IRC stream.
pub fn new(
config: &Config,
inner: Framed<T, IrcCodec>,
tx: UnboundedSender<Message>,
) -> Transport<T> {
let pinger = Some(Pinger::new(tx, config));
Transport { inner, pinger }
} }
fn close(&mut self) -> Poll<(), Self::SinkError> { /// Gets the inner stream underlying the `Transport`.
self.inner.close().map_err(|e| e.into()) pub fn into_inner(self) -> Framed<T, IrcCodec> {
self.inner
}
}
impl<T> Stream for Transport<T>
where
T: Unpin + AsyncRead + AsyncWrite,
{
type Item = Result<Message, error::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
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<T> Sink<Message> for Transport<T>
where
T: Unpin + AsyncRead + AsyncWrite,
{
type Error = error::Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
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<Result<(), Self::Error>> {
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<Result<(), Self::Error>> {
ready!(Pin::new(&mut self.as_mut().inner).poll_close(cx))?;
Poll::Ready(Ok(()))
} }
} }
@ -249,25 +216,30 @@ pub struct LogView {
impl LogView { impl LogView {
/// Gets a read guard for all the messages sent on the transport. /// Gets a read guard for all the messages sent on the transport.
pub fn sent(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> { pub fn sent(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> {
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. /// Gets a read guard for all the messages received on the transport.
pub fn received(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> { pub fn received(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> {
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. /// Note: this will introduce some performance overhead by cloning all messages.
pub struct Logged<T> where T: AsyncRead + AsyncWrite { pub struct Logged<T> {
inner: IrcTransport<T>, inner: Transport<T>,
view: LogView, view: LogView,
} }
impl<T> Logged<T> where T: AsyncRead + AsyncWrite { impl<T> Unpin for Logged<T> where T: Unpin {}
/// Wraps the given `IrcTransport` in logging.
pub fn wrap(inner: IrcTransport<T>) -> Logged<T> { impl<T> Logged<T>
where
T: AsyncRead + AsyncWrite,
{
/// Wraps the given `Transport` in logging.
pub fn wrap(inner: Transport<T>) -> Logged<T> {
Logged { Logged {
inner, inner,
view: LogView { view: LogView {
@ -285,42 +257,55 @@ impl<T> Logged<T> where T: AsyncRead + AsyncWrite {
impl<T> Stream for Logged<T> impl<T> Stream for Logged<T>
where where
T: AsyncRead + AsyncWrite, T: Unpin + AsyncRead + AsyncWrite,
{ {
type Item = Message; type Item = Result<Message, error::Error>;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match try_ready!(self.inner.poll()) { match ready!(Pin::new(&mut self.as_mut().inner).poll_next(cx)) {
Some(msg) => { Some(msg) => {
let recv: error::Result<_> = self.view.received.write().map_err(|_| { let msg = msg?;
error::IrcError::PoisonedLog
}); self.view
recv?.push(msg.clone()); .received
Ok(Async::Ready(Some(msg))) .write()
.map_err(|_| error::Error::PoisonedLog)?
.push(msg.clone());
Poll::Ready(Some(Ok(msg)))
} }
None => Ok(Async::Ready(None)), None => Poll::Ready(None),
} }
} }
} }
impl<T> Sink for Logged<T> impl<T> Sink<Message> for Logged<T>
where where
T: AsyncRead + AsyncWrite, T: Unpin + AsyncRead + AsyncWrite,
{ {
type SinkItem = Message; type Error = error::Error;
type SinkError = error::IrcError;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> { fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let res = self.inner.start_send(item.clone())?; Pin::new(&mut self.as_mut().inner).poll_ready(cx)
let sent: error::Result<_> = self.view.sent.write().map_err(|_| {
error::IrcError::PoisonedLog
});
sent?.push(item);
Ok(res)
} }
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Ok(self.inner.poll_complete()?) 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<Result<(), Self::Error>> {
Pin::new(&mut self.as_mut().inner).poll_flush(cx)
} }
} }

View file

@ -4,29 +4,28 @@ use std::io::Error as IoError;
use std::sync::mpsc::RecvError; use std::sync::mpsc::RecvError;
use failure; use failure;
use futures::sync::mpsc::SendError; use futures_channel::{
use futures::sync::oneshot::Canceled; mpsc::{SendError, TrySendError},
oneshot::Canceled,
};
use native_tls::Error as TlsError; use native_tls::Error as TlsError;
#[cfg(feature = "json")] #[cfg(feature = "json")]
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
use serde_yaml::Error as YamlError; use serde_yaml::Error as YamlError;
use tokio::executor::SpawnError;
use tokio_timer::TimerError;
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
use toml::de::Error as TomlReadError; use toml::de::Error as TomlReadError;
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
use toml::ser::Error as TomlWriteError; use toml::ser::Error as TomlWriteError;
use proto::Message; use crate::proto::error::{MessageParseError, ProtocolError};
use proto::error::{ProtocolError, MessageParseError};
/// A specialized `Result` type for the `irc` crate. /// A specialized `Result` type for the `irc` crate.
pub type Result<T> = ::std::result::Result<T, IrcError>; pub type Result<T> = ::std::result::Result<T, Error>;
/// The main crate-wide error type. /// The main crate-wide error type.
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
pub enum IrcError { pub enum Error {
/// An internal I/O error. /// An internal I/O error.
#[fail(display = "an io error occurred")] #[fail(display = "an io error occurred")]
Io(#[cause] IoError), Io(#[cause] IoError),
@ -35,26 +34,18 @@ pub enum IrcError {
#[fail(display = "a TLS error occurred")] #[fail(display = "a TLS error occurred")]
Tls(#[cause] TlsError), 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. /// An internal synchronous channel closed.
#[fail(display = "a sync channel closed")] #[fail(display = "a sync channel closed")]
SyncChannelClosed(#[cause] RecvError), SyncChannelClosed(#[cause] RecvError),
/// An internal asynchronous channel closed. /// An internal asynchronous channel closed.
#[fail(display = "an async channel closed")] #[fail(display = "an async channel closed")]
AsyncChannelClosed(#[cause] SendError<Message>), AsyncChannelClosed(#[cause] SendError),
/// An internal oneshot channel closed. /// An internal oneshot channel closed.
#[fail(display = "a oneshot channel closed")] #[fail(display = "a oneshot channel closed")]
OneShotCanceled(#[cause] Canceled), OneShotCanceled(#[cause] Canceled),
/// An internal timer error.
#[fail(display = "timer failed")]
Timer(#[cause] TimerError),
/// Error for invalid configurations. /// Error for invalid configurations.
#[fail(display = "invalid config: {}", path)] #[fail(display = "invalid config: {}", path)]
InvalidConfig { InvalidConfig {
@ -103,13 +94,17 @@ pub enum IrcError {
#[fail(display = "none of the specified nicknames were usable")] #[fail(display = "none of the specified nicknames were usable")]
NoUsableNick, 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 /// 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 /// the irc crate. No errors of this kind will ever be produced by the crate
/// itself. /// itself.
#[fail(display = "{}", inner)] #[fail(display = "{}", inner)]
Custom { Custom {
/// The actual error that occurred. /// The actual error that occurred.
inner: failure::Error inner: failure::Error,
}, },
} }
@ -170,55 +165,49 @@ pub enum TomlError {
Write(#[cause] TomlWriteError), Write(#[cause] TomlWriteError),
} }
impl From<ProtocolError> for IrcError { impl From<ProtocolError> for Error {
fn from(e: ProtocolError) -> IrcError { fn from(e: ProtocolError) -> Error {
match e { match e {
ProtocolError::Io(e) => IrcError::Io(e), ProtocolError::Io(e) => Error::Io(e),
ProtocolError::InvalidMessage { string, cause } => IrcError::InvalidMessage { ProtocolError::InvalidMessage { string, cause } => {
string, cause Error::InvalidMessage { string, cause }
}, }
} }
} }
} }
impl From<IoError> for IrcError { impl From<IoError> for Error {
fn from(e: IoError) -> IrcError { fn from(e: IoError) -> Error {
IrcError::Io(e) Error::Io(e)
} }
} }
impl From<TlsError> for IrcError { impl From<TlsError> for Error {
fn from(e: TlsError) -> IrcError { fn from(e: TlsError) -> Error {
IrcError::Tls(e) Error::Tls(e)
} }
} }
impl From<SpawnError> for IrcError { impl From<RecvError> for Error {
fn from(e: SpawnError) -> IrcError { fn from(e: RecvError) -> Error {
IrcError::Spawn(e) Error::SyncChannelClosed(e)
} }
} }
impl From<RecvError> for IrcError { impl From<SendError> for Error {
fn from(e: RecvError) -> IrcError { fn from(e: SendError) -> Error {
IrcError::SyncChannelClosed(e) Error::AsyncChannelClosed(e)
} }
} }
impl From<SendError<Message>> for IrcError { impl<T> From<TrySendError<T>> for Error {
fn from(e: SendError<Message>) -> IrcError { fn from(e: TrySendError<T>) -> Error {
IrcError::AsyncChannelClosed(e) Error::AsyncChannelClosed(e.into_send_error())
} }
} }
impl From<Canceled> for IrcError { impl From<Canceled> for Error {
fn from(e: Canceled) -> IrcError { fn from(e: Canceled) -> Error {
IrcError::OneShotCanceled(e) Error::OneShotCanceled(e)
}
}
impl From<TimerError> for IrcError {
fn from(e: TimerError) -> IrcError {
IrcError::Timer(e)
} }
} }

View file

@ -17,56 +17,36 @@
//! # Example //! # Example
//! //!
//! ```no_run //! ```no_run
//! # extern crate irc;
//! use irc::client::prelude::*; //! 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 //! // 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 //! // identify comes from ClientExt
//! client.identify().unwrap(); //! client.identify()?;
//! // for_each_incoming comes from Client //!
//! client.for_each_incoming(|irc_msg| { //! let mut stream = client.stream()?;
//! // irc_msg is a Message //!
//! if let Command::PRIVMSG(channel, message) = irc_msg.command { //! while let Some(message) = stream.next().await.transpose()? {
//! if let Command::PRIVMSG(channel, message) = message.command {
//! if message.contains(&*client.current_nickname()) { //! if message.contains(&*client.current_nickname()) {
//! // send_privmsg comes from ClientExt //! // send_privmsg comes from ClientExt
//! client.send_privmsg(&channel, "beep boop").unwrap(); //! client.send_privmsg(&channel, "beep boop").unwrap();
//! } //! }
//! } //! }
//! }).unwrap(); //! }
//! # Ok(())
//! # } //! # }
//! ``` //! ```
#![warn(missing_docs)] #![warn(missing_docs)]
extern crate bufstream;
extern crate bytes;
extern crate chrono;
#[macro_use] #[macro_use]
extern crate failure; extern crate failure;
extern crate encoding;
#[macro_use]
extern crate futures;
pub extern crate irc_proto as proto; 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 client;
pub mod error; pub mod error;