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
/Cargo.lock
/irc-proto/Cargo.lock
*.json
*.toml
*.yaml
!Cargo.toml

View file

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

View file

@ -9,6 +9,7 @@ categories = ["asynchronous", "network-programming"]
documentation = "https://docs.rs/irc/"
repository = "https://github.com/aatxe/irc"
readme = "README.md"
edition = "2018"
[badges]
travis-ci = { repository = "aatxe/irc" }
@ -31,22 +32,25 @@ bytes = "0.4"
chrono = "0.4"
encoding = "0.2"
failure = "0.1"
futures = "0.1"
irc-proto = { version = "*", path = "irc-proto" }
log = "0.4"
native-tls = "0.2"
serde = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
tokio = { version = "0.2.4", features = ["time", "net", "stream", "macros", "stream"] }
tokio-util = { version = "0.2.0", features = ["codec"] }
tokio-tls = "0.3.0"
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.7", optional = true }
tokio = "0.1"
tokio-codec = "0.1"
tokio-io = "0.1"
tokio-mockstream = "1.1"
tokio-timer = "0.1"
tokio-tls = "0.2"
toml = { version = "0.4", optional = true }
pin-utils = "0.1.0-alpha.4"
parking_lot = "0.9.0"
futures-channel = "0.3.1"
futures-util = "0.3.1"
[dev-dependencies]
futures = "0.3.1"
anyhow = "1.0.13"
args = "2.0"
getopts = "0.2"
env_logger = "0.6.2"

View file

@ -48,21 +48,16 @@ documentation, and below.
[irc-prelude]: https://docs.rs/irc/*/irc/client/prelude/index.html
## A Tale of Two APIs
## Using Futures
### Reactors (The "New" API)
The release of v0.13 brought with it a new API called `IrcReactor` that enables easier multiserver
support and more graceful error handling. The general model is that you use the reactor to create
new `IrcClients`, register message handler functions, and finally block the thread to run the
clients with their respective handlers. Here's an example:
```rust,no_run
extern crate irc;
The release of v0.14 replaced all existing APIs with one based on async/await.
```rust,no_run,edition2018
use irc::client::prelude::*;
use futures::prelude::*;
fn main() {
#[tokio::main]
async fn main() -> Result<(), failure::Error> {
// We can also load the Config at runtime via Config::load("path/to/config.toml")
let config = Config {
nickname: Some("the-irc-crate".to_owned()),
@ -71,50 +66,16 @@ fn main() {
..Config::default()
};
let mut reactor = IrcReactor::new().unwrap();
let client = reactor.prepare_client_and_connect(config).unwrap();
client.identify().unwrap();
let mut client = Client::from_config(config).await?;
client.identify()?;
reactor.register_client_with_handler(client, |client, message| {
let mut stream = client.stream()?;
while let Some(message) = stream.next().await.transpose()? {
print!("{}", message);
// And here we can do whatever we want with the messages.
Ok(())
});
}
reactor.run().unwrap();
}
```
### Direct Style (The "Old" API)
The old API for connecting to an IRC server is still supported through the `IrcClient` type. It's
simpler for the most basic use case, but will panic upon encountering any sort of connection issues.
In general, it's recommended that users switch to the new API if possible. Nevertheless, here is an
example:
```rust,no_run
extern crate irc;
use std::default::Default;
use irc::client::prelude::*;
fn main() {
// We can also load the Config at runtime via Config::load("path/to/config.toml")
let cfg = Config {
nickname: Some(format!("the-irc-crate")),
server: Some(format!("irc.example.com")),
channels: Some(vec![format!("#test")]),
.. Default::default()
};
let client = IrcClient::from_config(cfg).unwrap();
client.identify().unwrap();
client.for_each_incoming(|message| {
print!("{}", message);
// And here we can do whatever we want with the messages.
}).unwrap()
Ok(())
}
```

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1,13 +1,10 @@
extern crate irc;
extern crate tokio_timer;
use std::default::Default;
use std::time::Duration;
use futures::prelude::*;
use irc::client::prelude::*;
use irc::error::IrcError;
use std::time::Duration;
// NOTE: this example is a conversion of `tweeter.rs` to an asynchronous style with `IrcReactor`.
fn main() {
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("mastodon".to_owned()),
server: Some("irc.mozilla.org".to_owned()),
@ -15,37 +12,13 @@ fn main() {
..Default::default()
};
// We need to create a reactor first and foremost
let mut reactor = IrcReactor::new().unwrap();
// and then create a client via its API.
let client = reactor.prepare_client_and_connect(config).unwrap();
// Then, we identify
client.identify().unwrap();
// and clone just as before.
let send_client = client.clone();
let client = Client::from_config(config).await?;
let sender = client.sender();
// Rather than spawn a thread that reads the messages separately, we register a handler with the
// reactor. just as in the original version, we don't do any real handling and instead just
// print the messages that are received.
reactor.register_client_with_handler(client, |_, message| {
print!("{}", message);
Ok(())
});
let mut interval = tokio::time::interval(Duration::from_secs(1)).fuse();
// We construct an interval using a wheel timer from tokio_timer. This interval will fire every
// ten seconds (and is roughly accurate to the second).
let send_interval = tokio_timer::wheel()
.tick_duration(Duration::from_secs(1))
.num_slots(256)
.build()
.interval(Duration::from_secs(10));
// And then spawn a new future that performs the given action each time it fires.
reactor.register_future(send_interval.map_err(IrcError::Timer).for_each(move |()| {
// Anything in here will happen every 10 seconds!
send_client.send_privmsg("#rust-spam", "AWOOOOOOOOOO")
}));
// Then, on the main thread, we finally run the reactor which blocks the program indefinitely.
reactor.run().unwrap();
loop {
let _ = interval.select_next_some().await;
sender.send_privmsg("#rust-spam", "AWOOOOOOOOOO")?;
}
}

View file

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

View file

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

View file

@ -8,10 +8,10 @@ pub trait ChannelExt {
impl<'a> ChannelExt for &'a str {
fn is_channel_name(&self) -> bool {
self.starts_with('#') ||
self.starts_with('&') ||
self.starts_with('+') ||
self.starts_with('!')
self.starts_with('#')
|| self.starts_with('&')
|| self.starts_with('+')
|| self.starts_with('!')
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -52,7 +52,7 @@ make_response! {
RPL_BOUNCE = 10,
/// Undefined format. (Source: Modern)
///
/// RPL_NONE is a dummy numeric. It does not have a defined use nor format.
/// RPL_NONE is a dummy numeric. It does not have a defined use nor format.
RPL_NONE = 300,
/// `302 :*1<reply> *( " " <reply> )` (Source: RFC2812)
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.
use std::fs::File;
use std::fmt;
use std::io::Read;
use encoding::EncoderTrap;
use encoding::label::encoding_from_whatwg_label;
use futures::{Async, Poll, Future, Sink, StartSend, Stream};
use native_tls::{Certificate, TlsConnector, Identity};
use tokio_codec::Decoder;
use tokio::net::{TcpStream};
use tokio::net::tcp::ConnectFuture;
use tokio_mockstream::MockStream;
use futures_channel::mpsc::UnboundedSender;
use futures_util::{sink::Sink, stream::Stream};
use native_tls::{Certificate, Identity, TlsConnector};
use std::{
fmt,
fs::File,
io::Read,
pin::Pin,
task::{Context, Poll},
};
use tokio::net::TcpStream;
use tokio_tls::{self, TlsStream};
use tokio_util::codec::Decoder;
use error;
use client::data::Config;
use client::transport::{IrcTransport, LogView, Logged};
use proto::{IrcCodec, Message};
use crate::{
client::{
data::Config,
transport::{LogView, Logged, Transport},
},
error,
proto::{IrcCodec, Message},
};
/// An IRC connection used internally by `IrcServer`.
pub enum Connection {
#[doc(hidden)]
Unsecured(IrcTransport<TcpStream>),
Unsecured(Transport<TcpStream>),
#[doc(hidden)]
Secured(IrcTransport<TlsStream<TcpStream>>),
Secured(Transport<TlsStream<TcpStream>>),
#[doc(hidden)]
Mock(Logged<MockStream>),
Mock(Logged<crate::client::mock::MockStream>),
}
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 {
/// 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() {
Ok(ConnectionFuture::Mock(config))
} else if config.use_ssl() {
use encoding::{label::encoding_from_whatwg_label, EncoderTrap};
let encoding = encoding_from_whatwg_label(config.encoding()).ok_or_else(|| {
error::Error::UnknownCodec {
codec: config.encoding().to_owned(),
}
})?;
let init_str = config.mock_initial_value();
let initial = encoding
.encode(init_str, EncoderTrap::Replace)
.map_err(|data| error::Error::CodecFailed {
codec: encoding.name(),
data: data.into_owned(),
})?;
let stream = crate::client::mock::MockStream::new(&initial);
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = Transport::new(&config, framed, tx);
return Ok(Connection::Mock(Logged::wrap(transport)));
}
if config.use_ssl() {
let domain = format!("{}", config.server()?);
info!("Connecting via SSL to {}.", domain);
log::info!("Connecting via SSL to {}.", domain);
let mut builder = TlsConnector::builder();
if let Some(cert_path) = config.cert_path() {
let mut file = File::open(cert_path)?;
let mut cert_data = vec![];
file.read_to_end(&mut cert_data)?;
let cert = Certificate::from_der(&cert_data)?;
builder.add_root_certificate(cert);
info!("Added {} to trusted certificates.", cert_path);
log::info!("Added {} to trusted certificates.", cert_path);
}
if let Some(client_cert_path) = config.client_cert_path() {
let client_cert_pass = config.client_cert_pass();
let mut file = File::open(client_cert_path)?;
@ -145,25 +96,27 @@ impl Connection {
file.read_to_end(&mut client_cert_data)?;
let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?;
builder.identity(pkcs12_archive);
info!("Using {} for client certificate authentication.", client_cert_path);
log::info!(
"Using {} for client certificate authentication.",
client_cert_path
);
}
let connector: tokio_tls::TlsConnector = builder.build()?.into();
let stream = Box::new(TcpStream::connect(&config.socket_addr()?).map_err(|e| {
let res: error::IrcError = e.into();
res
}).and_then(move |socket| {
connector.connect(&domain, socket).map_err(
|e| e.into(),
)
}));
Ok(ConnectionFuture::Secured(config, stream))
let socket = TcpStream::connect(&config.socket_addr()?).await?;
let stream = connector.connect(&domain, socket).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = Transport::new(&config, framed, tx);
Ok(Connection::Secured(transport))
} else {
info!("Connecting to {}.", config.server()?);
log::info!("Connecting to {}.", config.server()?);
let addr = config.socket_addr()?;
Ok(ConnectionFuture::Unsecured(
config,
TcpStream::connect(&addr),
))
let stream = TcpStream::connect(&addr).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = Transport::new(&config, framed, tx);
Ok(Connection::Unsecured(transport))
}
}
@ -178,35 +131,49 @@ impl Connection {
}
impl Stream for Connection {
type Item = Message;
type Error = error::IrcError;
type Item = error::Result<Message>;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
match *self {
Connection::Unsecured(ref mut inner) => inner.poll(),
Connection::Secured(ref mut inner) => inner.poll(),
Connection::Mock(ref mut inner) => inner.poll(),
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_next(cx),
Connection::Secured(inner) => Pin::new(inner).poll_next(cx),
Connection::Mock(inner) => Pin::new(inner).poll_next(cx),
}
}
}
impl Sink for Connection {
type SinkItem = Message;
type SinkError = error::IrcError;
impl Sink<Message> for Connection {
type Error = error::Error;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
match *self {
Connection::Unsecured(ref mut inner) => inner.start_send(item),
Connection::Secured(ref mut inner) => inner.start_send(item),
Connection::Mock(ref mut inner) => inner.start_send(item),
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_ready(cx),
Connection::Secured(inner) => Pin::new(inner).poll_ready(cx),
Connection::Mock(inner) => Pin::new(inner).poll_ready(cx),
}
}
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
match *self {
Connection::Unsecured(ref mut inner) => inner.poll_complete(),
Connection::Secured(ref mut inner) => inner.poll_complete(),
Connection::Mock(ref mut inner) => inner.poll_complete(),
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).start_send(item),
Connection::Secured(inner) => Pin::new(inner).start_send(item),
Connection::Mock(inner) => Pin::new(inner).start_send(item),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<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
use std::borrow::ToOwned;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::File,
io::prelude::*,
net::{SocketAddr, ToSocketAddrs},
path::{Path, PathBuf},
};
#[cfg(feature = "json")]
use serde_json;
@ -13,10 +15,10 @@ use serde_yaml;
#[cfg(feature = "toml")]
use toml;
use crate::error::Error::InvalidConfig;
#[cfg(feature = "toml")]
use error::TomlError;
use error::{ConfigError, Result};
use error::IrcError::InvalidConfig;
use crate::error::TomlError;
use crate::error::{ConfigError, Result};
/// Configuration for IRC clients.
///
@ -153,9 +155,10 @@ impl Config {
}
fn path(&self) -> String {
self.path.as_ref().map(|buf| buf.to_string_lossy().into_owned()).unwrap_or_else(|| {
"<none>".to_owned()
})
self.path
.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
@ -182,68 +185,54 @@ impl Config {
}),
};
res.map(|config| {
config.with_path(path)
})
res.map(|config| config.with_path(path))
}
#[cfg(feature = "json")]
fn load_json<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
serde_json::from_str(&data[..]).map_err(|e| {
InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidJson(e),
}
fn load_json<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
serde_json::from_str(data).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidJson(e),
})
}
#[cfg(not(feature = "json"))]
fn load_json<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> {
fn load_json<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled {
format: "JSON"
}
cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
})
}
#[cfg(feature = "toml")]
fn load_toml<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
toml::from_str(&data[..]).map_err(|e| {
InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidToml(TomlError::Read(e)),
}
fn load_toml<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
toml::from_str(data).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidToml(TomlError::Read(e)),
})
}
#[cfg(not(feature = "toml"))]
fn load_toml<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> {
fn load_toml<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled {
format: "TOML"
}
cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
})
}
#[cfg(feature = "yaml")]
fn load_yaml<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
serde_yaml::from_str(&data[..]).map_err(|e| {
InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidYaml(e),
}
fn load_yaml<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
serde_yaml::from_str(data).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidYaml(e),
})
}
#[cfg(not(feature = "yaml"))]
fn load_yaml<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> {
fn load_yaml<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled {
format: "YAML"
}
cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
})
}
@ -257,16 +246,20 @@ impl Config {
Some("json") => self.save_json(&path)?,
Some("toml") => self.save_toml(&path)?,
Some("yaml") | Some("yml") => self.save_yaml(&path)?,
Some(ext) => return Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::UnknownConfigFormat {
format: ext.to_owned(),
},
}),
None => return Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::MissingExtension,
}),
Some(ext) => {
return Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::UnknownConfigFormat {
format: ext.to_owned(),
},
})
}
None => {
return Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::MissingExtension,
})
}
};
file.write_all(data.as_bytes())?;
self.path = Some(path.as_ref().to_owned());
@ -275,11 +268,9 @@ impl Config {
#[cfg(feature = "json")]
fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
serde_json::to_string(self).map_err(|e| {
InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidJson(e),
}
serde_json::to_string(self).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidJson(e),
})
}
@ -287,19 +278,15 @@ impl Config {
fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled {
format: "JSON"
}
cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
})
}
#[cfg(feature = "toml")]
fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
toml::to_string(self).map_err(|e| {
InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidToml(TomlError::Write(e)),
}
toml::to_string(self).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidToml(TomlError::Write(e)),
})
}
@ -307,19 +294,15 @@ impl Config {
fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled {
format: "TOML"
}
cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
})
}
#[cfg(feature = "yaml")]
fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
serde_yaml::to_string(self).map_err(|e| {
InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidYaml(e),
}
serde_yaml::to_string(self).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidYaml(e),
})
}
@ -327,9 +310,7 @@ impl Config {
fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled {
format: "YAML"
}
cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
})
}
@ -343,12 +324,13 @@ impl Config {
/// Gets the nickname specified in the configuration.
pub fn nickname(&self) -> Result<&str> {
self.nickname.as_ref().map(|s| &s[..]).ok_or_else(|| {
InvalidConfig {
self.nickname
.as_ref()
.map(|s| &s[..])
.ok_or_else(|| InvalidConfig {
path: self.path(),
cause: ConfigError::NicknameNotSpecified,
}
})
})
}
/// Gets the bot's nickserv password specified in the configuration.
@ -360,48 +342,52 @@ impl Config {
/// Gets the alternate nicknames specified in the configuration.
/// This defaults to an empty vector when not specified.
pub fn alternate_nicknames(&self) -> Vec<&str> {
self.alt_nicks.as_ref().map_or(vec![], |v| {
v.iter().map(|s| &s[..]).collect()
})
self.alt_nicks
.as_ref()
.map_or(vec![], |v| v.iter().map(|s| &s[..]).collect())
}
/// Gets the username specified in the configuration.
/// This defaults to the user's nickname when not specified.
pub fn username(&self) -> &str {
self.username.as_ref().map_or(self.nickname().unwrap_or("user"), |s| &s)
self.username
.as_ref()
.map_or(self.nickname().unwrap_or("user"), |s| &s)
}
/// Gets the real name specified in the configuration.
/// This defaults to the user's nickname when not specified.
pub fn real_name(&self) -> &str {
self.realname.as_ref().map_or(self.nickname().unwrap_or("irc"), |s| &s)
self.realname
.as_ref()
.map_or(self.nickname().unwrap_or("irc"), |s| &s)
}
/// Gets the address of the server specified in the configuration.
pub fn server(&self) -> Result<&str> {
self.server.as_ref().map(|s| &s[..]).ok_or_else(|| {
InvalidConfig {
self.server
.as_ref()
.map(|s| &s[..])
.ok_or_else(|| InvalidConfig {
path: self.path(),
cause: ConfigError::ServerNotSpecified,
}
})
})
}
/// Gets the port of the server specified in the configuration.
/// This defaults to 6667 (or 6697 if use_ssl is specified as true) when not specified.
pub fn port(&self) -> u16 {
self.port.as_ref().cloned().unwrap_or(if self.use_ssl() {
6697
} else {
6667
})
self.port
.as_ref()
.cloned()
.unwrap_or(if self.use_ssl() { 6697 } else { 6667 })
}
/// Gets the server and port as a `SocketAddr`.
/// This panics when server is not specified or the address is malformed.
pub fn socket_addr(&self) -> Result<SocketAddr> {
format!("{}:{}", self.server()?, self.port()).to_socket_addrs()
format!("{}:{}", self.server()?, self.port())
.to_socket_addrs()
.map(|mut i| i.next().unwrap())
.map_err(|e| e.into())
}
@ -442,17 +428,16 @@ impl Config {
/// Gets the channels to join upon connection.
/// This defaults to an empty vector if it's not specified.
pub fn channels(&self) -> Vec<&str> {
self.channels.as_ref().map_or(vec![], |v| {
v.iter().map(|s| &s[..]).collect()
})
self.channels
.as_ref()
.map_or(vec![], |v| v.iter().map(|s| &s[..]).collect())
}
/// Gets the key for the specified channel if it exists in the configuration.
pub fn channel_key(&self, chan: &str) -> Option<&str> {
self.channel_keys.as_ref().and_then(|m| {
m.get(&chan.to_owned()).map(|s| &s[..])
})
self.channel_keys
.as_ref()
.and_then(|m| m.get(&chan.to_owned()).map(|s| &s[..]))
}
/// Gets the user modes to set on connect specified in the configuration.
@ -471,16 +456,15 @@ impl Config {
/// This defaults to `irc:version:env` when not specified.
/// For example, `irc:0.12.0:Compiled with rustc`
pub fn version(&self) -> &str {
self.version.as_ref().map_or(::VERSION_STR, |s| &s)
self.version.as_ref().map_or(crate::VERSION_STR, |s| &s)
}
/// Gets the string to be sent in response to CTCP SOURCE requests.
/// This defaults to `https://github.com/aatxe/irc` when not specified.
pub fn source(&self) -> &str {
self.source.as_ref().map_or(
"https://github.com/aatxe/irc",
|s| &s[..],
)
self.source
.as_ref()
.map_or("https://github.com/aatxe/irc", |s| &s[..])
}
/// Gets the amount of time in seconds for the interval at which the client pings the server.
@ -522,16 +506,16 @@ impl Config {
/// Gets the NickServ command sequence to recover a nickname.
/// This defaults to `["GHOST"]` when not specified.
pub fn ghost_sequence(&self) -> Vec<&str> {
self.ghost_sequence.as_ref().map_or(vec!["GHOST"], |v| {
v.iter().map(|s| &s[..]).collect()
})
self.ghost_sequence
.as_ref()
.map_or(vec!["GHOST"], |v| v.iter().map(|s| &s[..]).collect())
}
/// Looks up the specified string in the options map.
pub fn get_option(&self, option: &str) -> Option<&str> {
self.options.as_ref().and_then(|o| {
o.get(&option.to_owned()).map(|s| &s[..])
})
self.options
.as_ref()
.and_then(|o| o.get(&option.to_owned()).map(|s| &s[..]))
}
/// Gets whether or not to use a mock connection for testing.
@ -550,12 +534,8 @@ impl Config {
#[cfg(test)]
mod test {
use std::collections::HashMap;
use std::default::Default;
#[cfg(feature = "json")]
use std::path::Path;
use super::Config;
use std::collections::HashMap;
#[allow(unused)]
fn test_config() -> Config {
@ -594,24 +574,6 @@ mod test {
}
}
#[test]
#[cfg(feature = "json")]
fn load_from_json() {
assert_eq!(Config::load("client_config.json").unwrap(), test_config().with_path("client_config.json"));
}
#[test]
#[cfg(feature = "toml")]
fn load_from_toml() {
assert_eq!(Config::load("client_config.toml").unwrap(), test_config().with_path("client_config.toml"));
}
#[test]
#[cfg(feature = "yaml")]
fn load_from_yaml() {
assert_eq!(Config::load("client_config.yaml").unwrap(), test_config().with_path("client_config.yaml"));
}
#[test]
fn is_owner() {
let cfg = Config {
@ -636,4 +598,37 @@ mod test {
assert_eq!(cfg.get_option("testing"), Some("test"));
assert_eq!(cfg.get_option("not"), None);
}
#[test]
#[cfg(feature = "json")]
fn load_from_json() -> Result<(), failure::Error> {
const DATA: &str = include_str!("client_config.json");
assert_eq!(
Config::load_json("client_config.json", DATA)?.with_path("client_config.json"),
test_config().with_path("client_config.json")
);
Ok(())
}
#[test]
#[cfg(feature = "toml")]
fn load_from_toml() -> Result<(), failure::Error> {
const DATA: &str = include_str!("client_config.toml");
assert_eq!(
Config::load_toml("client_config.toml", DATA)?.with_path("client_config.toml"),
test_config().with_path("client_config.toml")
);
Ok(())
}
#[test]
#[cfg(feature = "yaml")]
fn load_from_yaml() -> Result<(), failure::Error> {
const DATA: &str = include_str!("client_config.yaml");
assert_eq!(
Config::load_yaml("client_config.yaml", DATA)?.with_path("client_config.yaml"),
test_config().with_path("client_config.yaml")
);
Ok(())
}
}

View file

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

View file

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

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,
//! as well as in the parsed form of received mode commands.
pub use client::data::Config;
pub use client::reactor::IrcReactor;
pub use client::{EachIncomingExt, IrcClient, Client};
pub use client::ext::ClientExt;
pub use proto::{Capability, ChannelExt, Command, Message, Prefix, NegotiationVersion, Response};
pub use proto::{ChannelMode, Mode, UserMode};
pub use futures::{Future, Stream};
pub use crate::{
client::{data::Config, Client, Sender},
proto::{
Capability, ChannelExt, ChannelMode, Command, Message, Mode, NegotiationVersion, Prefix,
Response, UserMode,
},
};

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
//! automatic PING replies, automatic sending of PINGs, and message rate-limiting. This can be used
//! as the basis for implementing a more full IRC client.
use std::collections::VecDeque;
use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::time::{Duration, Instant};
use std::{
pin::Pin,
sync::{Arc, RwLock, RwLockReadGuard},
task::{Context, Poll},
time::Duration,
};
use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream};
use chrono::prelude::*;
use tokio_codec::Framed;
use tokio_io::{AsyncRead, AsyncWrite};
use tokio_timer;
use tokio_timer::{Interval, Sleep, Timer};
use futures_channel::mpsc::UnboundedSender;
use futures_util::{future::Future, ready, sink::Sink, stream::Stream};
use tokio::{
io::{AsyncRead, AsyncWrite},
time::{self, Delay, Interval},
};
use tokio_util::codec::Framed;
use error;
use client::data::Config;
use proto::{Command, IrcCodec, Message};
use crate::{
client::data::Config,
error,
proto::{Command, IrcCodec, Message},
};
/// An IRC transport that handles core functionality for the IRC protocol. This is used in the
/// implementation of `Connection` and ultimately `IrcServer`, and plays an important role in
/// handling connection timeouts, message throttling, and ping response.
pub struct IrcTransport<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,
/// Pinger-based futures helper.
struct Pinger {
tx: UnboundedSender<Message>,
/// The amount of time to wait before timing out from no ping response.
ping_timeout: u64,
/// The last data sent with a ping.
last_ping_data: String,
ping_timeout: Duration,
/// The instant that the last ping was sent to the server.
last_ping_sent: Instant,
/// The instant that the last pong was received from the server.
last_pong_received: Instant,
ping_deadline: Option<Delay>,
/// The interval at which to send pings.
ping_interval: Interval,
}
impl<T> IrcTransport<T>
where
T: AsyncRead + AsyncWrite,
{
/// Creates a new `IrcTransport` from the given IRC stream.
pub fn new(config: &Config, inner: Framed<T, IrcCodec>) -> IrcTransport<T> {
let timer = tokio_timer::wheel().build();
IrcTransport {
inner,
burst_timer: tokio_timer::wheel().build(),
rolling_burst_window: VecDeque::new(),
burst_window_length: u64::from(config.burst_window_length()),
max_burst_messages: u64::from(config.max_messages_in_burst()),
current_burst_messages: 0,
ping_timer: timer.interval(Duration::from_secs(u64::from(config.ping_time()))),
ping_timeout: u64::from(config.ping_timeout()),
last_ping_data: String::new(),
last_ping_sent: Instant::now(),
last_pong_received: Instant::now(),
impl Pinger {
/// Construct a new pinger helper.
pub fn new(tx: UnboundedSender<Message>, config: &Config) -> Pinger {
let ping_timeout = Duration::from_secs(u64::from(config.ping_timeout()));
Self {
tx,
ping_timeout,
ping_deadline: None,
ping_interval: time::interval(ping_timeout / 2),
}
}
/// Gets the inner stream underlying the `IrcTransport`.
pub fn into_inner(self) -> Framed<T, IrcCodec> {
self.inner
/// Handle an incoming message.
fn handle_message(&mut self, message: &Message) -> error::Result<()> {
match message.command {
// On receiving a `PING` message from the server, we automatically respond with
// the appropriate `PONG` message to keep the connection alive for transport.
Command::PING(ref data, _) => {
self.send_pong(data)?;
}
// Check `PONG` responses from the server. If it matches, we will update the
// last instant that the pong was received. This will prevent timeout.
Command::PONG(_, None) | Command::PONG(_, Some(_)) => {
log::trace!("Received PONG");
self.ping_deadline.take();
}
_ => (),
}
Ok(())
}
/// Determines whether or not the transport has hit the ping timeout.
fn ping_timed_out(&self) -> bool {
if self.last_pong_received < self.last_ping_sent {
self.last_ping_sent.elapsed().as_secs() >= self.ping_timeout
} else {
false
}
/// Send a pong.
fn send_pong(&mut self, data: &str) -> error::Result<()> {
self.tx
.unbounded_send(Command::PONG(data.to_owned(), None).into())?;
Ok(())
}
/// Sends a ping via the transport.
fn send_ping(&mut self) -> error::Result<()> {
log::trace!("Sending PING");
// Creates new ping data using the local timestamp.
let last_ping_data = format!("{}", Local::now().timestamp());
let data = last_ping_data.clone();
let result = self.start_send(Command::PING(data, None).into())?;
if let AsyncSink::Ready = result {
self.poll_complete()?;
// If we succeeded in sending the ping, we will update when the last ping was sent, and
// the data that was sent with it.
self.last_ping_sent = Instant::now();
self.last_ping_data = last_ping_data;
}
let data = format!("{}", Local::now().timestamp());
self.tx
.unbounded_send(Command::PING(data.clone(), None).into())?;
Ok(())
}
/// Polls the most recent burst window from the queue, always returning `NotReady` if none are
/// left for whatever reason.
fn rolling_burst_window_front(&mut self) -> Result<Async<()>, tokio_timer::TimerError> {
self.rolling_burst_window.front_mut().map(|w| w.poll()).unwrap_or(Ok(Async::NotReady))
}
}
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)))
}
/// Set the ping deadline.
fn set_deadline(&mut self) {
if self.ping_deadline.is_none() {
let ping_deadline = time::delay_for(self.ping_timeout);
self.ping_deadline = Some(ping_deadline);
}
}
}
impl<T> Sink for IrcTransport<T>
where
T: AsyncRead + AsyncWrite,
{
type SinkItem = Message;
type SinkError = error::IrcError;
impl Future for Pinger {
type Output = Result<(), error::Error>;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
// If the ping timeout has been reached, we close the connection out and return an error.
if self.ping_timed_out() {
self.close()?;
return Err(error::IrcError::PingTimeout)
}
// Check if the oldest message in the rolling window is discounted.
if let Async::Ready(()) = self.rolling_burst_window_front()? {
self.current_burst_messages -= 1;
self.rolling_burst_window.pop_front();
}
// Throttling if too many messages have been sent recently.
if self.current_burst_messages >= self.max_burst_messages {
// When throttled, we know we need to finish sending what's already queued up.
self.poll_complete()?;
return Ok(AsyncSink::NotReady(item))
}
match self.inner.start_send(item)? {
AsyncSink::NotReady(item) => Ok(AsyncSink::NotReady(item)),
AsyncSink::Ready => {
self.current_burst_messages += 1;
self.rolling_burst_window.push_back(self.burst_timer.sleep(Duration::from_secs(
self.burst_window_length
)));
Ok(AsyncSink::Ready)
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(ping_deadline) = self.as_mut().ping_deadline.as_mut() {
match Pin::new(ping_deadline).poll(cx) {
Poll::Ready(()) => return Poll::Ready(Err(error::Error::PingTimeout)),
Poll::Pending => (),
}
}
}
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
// If the ping timeout has been reached, we close the connection out and return an error.
if self.ping_timed_out() {
self.close()?;
return Err(error::IrcError::PingTimeout)
if let Poll::Ready(_) = Pin::new(&mut self.as_mut().ping_interval).poll_next(cx) {
self.as_mut().send_ping()?;
self.as_mut().set_deadline();
}
// If it's time to send a ping, we should do it! This is necessary to ensure that the
// sink half will close even if the stream half closed without a ping timeout.
if let Async::Ready(msg) = self.ping_timer.poll()? {
assert!(msg.is_some());
self.send_ping()?;
}
Poll::Pending
}
}
Ok(self.inner.poll_complete()?)
/// An IRC transport that handles core functionality for the IRC protocol. This is used in the
/// implementation of `Connection` and ultimately `IrcServer`, and plays an important role in
/// handling connection timeouts, message throttling, and ping response.
pub struct Transport<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> {
self.inner.close().map_err(|e| e.into())
/// Gets the inner stream underlying the `Transport`.
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 {
/// Gets a read guard for all the messages sent on the transport.
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.
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.
pub struct Logged<T> where T: AsyncRead + AsyncWrite {
inner: IrcTransport<T>,
pub struct Logged<T> {
inner: Transport<T>,
view: LogView,
}
impl<T> Logged<T> where T: AsyncRead + AsyncWrite {
/// Wraps the given `IrcTransport` in logging.
pub fn wrap(inner: IrcTransport<T>) -> Logged<T> {
impl<T> Unpin for Logged<T> where T: Unpin {}
impl<T> Logged<T>
where
T: AsyncRead + AsyncWrite,
{
/// Wraps the given `Transport` in logging.
pub fn wrap(inner: Transport<T>) -> Logged<T> {
Logged {
inner,
view: LogView {
@ -285,42 +257,55 @@ impl<T> Logged<T> where T: AsyncRead + AsyncWrite {
impl<T> Stream for Logged<T>
where
T: AsyncRead + AsyncWrite,
T: Unpin + AsyncRead + AsyncWrite,
{
type Item = Message;
type Error = error::IrcError;
type Item = Result<Message, error::Error>;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
match try_ready!(self.inner.poll()) {
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(Pin::new(&mut self.as_mut().inner).poll_next(cx)) {
Some(msg) => {
let recv: error::Result<_> = self.view.received.write().map_err(|_| {
error::IrcError::PoisonedLog
});
recv?.push(msg.clone());
Ok(Async::Ready(Some(msg)))
let msg = msg?;
self.view
.received
.write()
.map_err(|_| error::Error::PoisonedLog)?
.push(msg.clone());
Poll::Ready(Some(Ok(msg)))
}
None => Ok(Async::Ready(None)),
None => Poll::Ready(None),
}
}
}
impl<T> Sink for Logged<T>
impl<T> Sink<Message> for Logged<T>
where
T: AsyncRead + AsyncWrite,
T: Unpin + AsyncRead + AsyncWrite,
{
type SinkItem = Message;
type SinkError = error::IrcError;
type Error = error::Error;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
let res = self.inner.start_send(item.clone())?;
let sent: error::Result<_> = self.view.sent.write().map_err(|_| {
error::IrcError::PoisonedLog
});
sent?.push(item);
Ok(res)
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.as_mut().inner).poll_ready(cx)
}
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
Ok(self.inner.poll_complete()?)
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
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 failure;
use futures::sync::mpsc::SendError;
use futures::sync::oneshot::Canceled;
use futures_channel::{
mpsc::{SendError, TrySendError},
oneshot::Canceled,
};
use native_tls::Error as TlsError;
#[cfg(feature = "json")]
use serde_json::Error as JsonError;
#[cfg(feature = "yaml")]
use serde_yaml::Error as YamlError;
use tokio::executor::SpawnError;
use tokio_timer::TimerError;
#[cfg(feature = "toml")]
use toml::de::Error as TomlReadError;
#[cfg(feature = "toml")]
use toml::ser::Error as TomlWriteError;
use proto::Message;
use proto::error::{ProtocolError, MessageParseError};
use crate::proto::error::{MessageParseError, ProtocolError};
/// A specialized `Result` type for the `irc` crate.
pub type Result<T> = ::std::result::Result<T, IrcError>;
pub type Result<T> = ::std::result::Result<T, Error>;
/// The main crate-wide error type.
#[derive(Debug, Fail)]
pub enum IrcError {
pub enum Error {
/// An internal I/O error.
#[fail(display = "an io error occurred")]
Io(#[cause] IoError),
@ -35,26 +34,18 @@ pub enum IrcError {
#[fail(display = "a TLS error occurred")]
Tls(#[cause] TlsError),
/// An error caused by Tokio being unable to spawn a task.
#[fail(display = "unable to spawn task")]
Spawn(#[cause] SpawnError),
/// An internal synchronous channel closed.
#[fail(display = "a sync channel closed")]
SyncChannelClosed(#[cause] RecvError),
/// An internal asynchronous channel closed.
#[fail(display = "an async channel closed")]
AsyncChannelClosed(#[cause] SendError<Message>),
AsyncChannelClosed(#[cause] SendError),
/// An internal oneshot channel closed.
#[fail(display = "a oneshot channel closed")]
OneShotCanceled(#[cause] Canceled),
/// An internal timer error.
#[fail(display = "timer failed")]
Timer(#[cause] TimerError),
/// Error for invalid configurations.
#[fail(display = "invalid config: {}", path)]
InvalidConfig {
@ -103,13 +94,17 @@ pub enum IrcError {
#[fail(display = "none of the specified nicknames were usable")]
NoUsableNick,
/// Stream has already been configured.
#[fail(display = "stream has already been configured")]
StreamAlreadyConfigured,
/// This allows you to produce any `failure::Error` within closures used by
/// the irc crate. No errors of this kind will ever be produced by the crate
/// itself.
#[fail(display = "{}", inner)]
Custom {
/// The actual error that occurred.
inner: failure::Error
inner: failure::Error,
},
}
@ -170,55 +165,49 @@ pub enum TomlError {
Write(#[cause] TomlWriteError),
}
impl From<ProtocolError> for IrcError {
fn from(e: ProtocolError) -> IrcError {
impl From<ProtocolError> for Error {
fn from(e: ProtocolError) -> Error {
match e {
ProtocolError::Io(e) => IrcError::Io(e),
ProtocolError::InvalidMessage { string, cause } => IrcError::InvalidMessage {
string, cause
},
ProtocolError::Io(e) => Error::Io(e),
ProtocolError::InvalidMessage { string, cause } => {
Error::InvalidMessage { string, cause }
}
}
}
}
impl From<IoError> for IrcError {
fn from(e: IoError) -> IrcError {
IrcError::Io(e)
impl From<IoError> for Error {
fn from(e: IoError) -> Error {
Error::Io(e)
}
}
impl From<TlsError> for IrcError {
fn from(e: TlsError) -> IrcError {
IrcError::Tls(e)
impl From<TlsError> for Error {
fn from(e: TlsError) -> Error {
Error::Tls(e)
}
}
impl From<SpawnError> for IrcError {
fn from(e: SpawnError) -> IrcError {
IrcError::Spawn(e)
impl From<RecvError> for Error {
fn from(e: RecvError) -> Error {
Error::SyncChannelClosed(e)
}
}
impl From<RecvError> for IrcError {
fn from(e: RecvError) -> IrcError {
IrcError::SyncChannelClosed(e)
impl From<SendError> for Error {
fn from(e: SendError) -> Error {
Error::AsyncChannelClosed(e)
}
}
impl From<SendError<Message>> for IrcError {
fn from(e: SendError<Message>) -> IrcError {
IrcError::AsyncChannelClosed(e)
impl<T> From<TrySendError<T>> for Error {
fn from(e: TrySendError<T>) -> Error {
Error::AsyncChannelClosed(e.into_send_error())
}
}
impl From<Canceled> for IrcError {
fn from(e: Canceled) -> IrcError {
IrcError::OneShotCanceled(e)
}
}
impl From<TimerError> for IrcError {
fn from(e: TimerError) -> IrcError {
IrcError::Timer(e)
impl From<Canceled> for Error {
fn from(e: Canceled) -> Error {
Error::OneShotCanceled(e)
}
}

View file

@ -17,56 +17,36 @@
//! # Example
//!
//! ```no_run
//! # extern crate irc;
//! use irc::client::prelude::*;
//! use futures::prelude::*;
//!
//! # fn main() {
//! # #[tokio::main]
//! # async fn main() -> irc::error::Result<()> {
//! // configuration is loaded from config.toml into a Config
//! let client = IrcClient::new("config.toml").unwrap();
//! let mut client = Client::new("config.toml").await?;
//! // identify comes from ClientExt
//! client.identify().unwrap();
//! // for_each_incoming comes from Client
//! client.for_each_incoming(|irc_msg| {
//! // irc_msg is a Message
//! if let Command::PRIVMSG(channel, message) = irc_msg.command {
//! client.identify()?;
//!
//! let mut stream = client.stream()?;
//!
//! while let Some(message) = stream.next().await.transpose()? {
//! if let Command::PRIVMSG(channel, message) = message.command {
//! if message.contains(&*client.current_nickname()) {
//! // send_privmsg comes from ClientExt
//! client.send_privmsg(&channel, "beep boop").unwrap();
//! }
//! }
//! }).unwrap();
//! }
//! # Ok(())
//! # }
//! ```
#![warn(missing_docs)]
extern crate bufstream;
extern crate bytes;
extern crate chrono;
#[macro_use]
extern crate failure;
extern crate encoding;
#[macro_use]
extern crate futures;
pub extern crate irc_proto as proto;
#[macro_use]
extern crate log;
extern crate native_tls;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[cfg(feature = "json")]
extern crate serde_json;
#[cfg(feature = "yaml")]
extern crate serde_yaml;
extern crate tokio;
extern crate tokio_codec;
extern crate tokio_io;
extern crate tokio_mockstream;
extern crate tokio_timer;
extern crate tokio_tls;
#[cfg(feature = "toml")]
extern crate toml;
pub mod client;
pub mod error;