Merged develop onto 0.14 (which was tricky, and this might have introduced bugs?).

This commit is contained in:
Aaron Weiss 2018-09-17 17:50:53 -04:00
parent c69e944033
commit 53fb890a7e
No known key found for this signature in database
GPG key ID: 047D32DF25DC22EF
21 changed files with 425 additions and 367 deletions

View file

@ -16,7 +16,7 @@ notifications:
email: false email: false
irc: irc:
channels: channels:
- "irc.fyrechat.net#vana-commits" - "ircs://irc.pdgn.co:6697/#commits"
template: template:
- "%{repository_slug}/%{branch} (%{commit} - %{author}): %{message}" - "%{repository_slug}/%{branch} (%{commit} - %{author}): %{message}"
skip_join: true skip_join: true

View file

@ -33,18 +33,18 @@ encoding = "0.2"
failure = "0.1" failure = "0.1"
futures = "0.1" futures = "0.1"
irc-proto = { version = "*", path = "irc-proto" } irc-proto = { version = "*", path = "irc-proto" }
log = "0.3" log = "0.4"
native-tls = "0.1" native-tls = "0.2"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = { version = "1.0", optional = true } serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.7", optional = true } serde_yaml = { version = "0.7", optional = true }
tokio = "0.1" tokio-codec = "0.1"
tokio-core = "0.1" tokio-core = "0.1"
tokio-io = "0.1" tokio-io = "0.1"
tokio-mockstream = "1.1" tokio-mockstream = "1.1"
tokio-timer = "0.1" tokio-timer = "0.1"
tokio-tls = "0.1" tokio-tls = "0.2"
toml = { version = "0.4", optional = true } toml = { version = "0.4", optional = true }
[dev-dependencies] [dev-dependencies]

View file

@ -39,7 +39,7 @@ Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pu
## Getting Started ## Getting Started
To start using the irc crate with cargo, you can simply add `irc = "0.13"` to your dependencies in To start using the irc crate with cargo, you can add `irc = "0.13"` to your dependencies in
your Cargo.toml file. The high-level API can be found in [`irc::client::prelude`][irc-prelude]. your Cargo.toml file. The high-level API can be found in [`irc::client::prelude`][irc-prelude].
You'll find a number of examples to help you get started in `examples/`, throughout the You'll find a number of examples to help you get started in `examples/`, throughout the
documentation, and below. documentation, and below.
@ -174,7 +174,7 @@ cargo run --example convertconf -- -i client_config.json -o client_config.toml
``` ```
Note that the formats are automatically determined based on the selected file extensions. This Note that the formats are automatically determined based on the selected file extensions. This
tool should make it easy for users to migrate their old configurations to TOML. tool should make it easier for users to migrate their old configurations to TOML.
## Contributing ## Contributing
the irc crate is a free, open source library that relies on contributions from its maintainers, the irc crate is a free, open source library that relies on contributions from its maintainers,

View file

@ -7,15 +7,15 @@ use irc::client::prelude::*;
fn main() { fn main() {
let cfg1 = Config { let cfg1 = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default() ..Default::default()
}; };
let cfg2 = Config { let cfg2 = Config {
nickname: Some("bananas".to_owned()), nickname: Some("bananas".to_owned()),
server: Some("irc.fyrechat.net".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default() ..Default::default()
}; };
@ -26,7 +26,7 @@ fn main() {
for config in configs { for config in configs {
// Immediate errors like failure to resolve the server's domain or to establish any connection will // Immediate errors like failure to resolve the server's domain or to establish any connection will
// manifest here in the result of prepare_client_and_connect. // manifest here in the result of prepare_client_and_connect.
let client = reactor.prepare_client_and_connect(config).unwrap(); let client = reactor.prepare_client_and_connect(&config).unwrap();
client.identify().unwrap(); client.identify().unwrap();
// Here, we tell the reactor to setup this client for future handling (in run) using the specified // Here, we tell the reactor to setup this client for future handling (in run) using the specified
// handler function process_msg. // handler function process_msg.

View file

@ -8,13 +8,13 @@ fn main() {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]), alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]),
server: Some("irc.fyrechat.net".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default() ..Default::default()
}; };
let mut reactor = IrcReactor::new().unwrap(); let mut reactor = IrcReactor::new().unwrap();
let client = reactor.prepare_client_and_connect(config).unwrap(); let client = reactor.prepare_client_and_connect(&config).unwrap();
client.identify().unwrap(); client.identify().unwrap();
reactor.register_client_with_handler(client, |client, message| { reactor.register_client_with_handler(client, |client, message| {

View file

@ -7,15 +7,15 @@ use irc::client::prelude::*;
fn main() { fn main() {
let cfg1 = Config { let cfg1 = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default() ..Default::default()
}; };
let cfg2 = Config { let cfg2 = Config {
nickname: Some("bananas".to_owned()), nickname: Some("bananas".to_owned()),
server: Some("irc.fyrechat.net".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default() ..Default::default()
}; };
@ -26,7 +26,7 @@ fn main() {
loop { loop {
let res = configs.iter().fold(Ok(()), |acc, config| { let res = configs.iter().fold(Ok(()), |acc, config| {
acc.and( acc.and(
reactor.prepare_client_and_connect(config.clone()).and_then(|client| { reactor.prepare_client_and_connect(&config).and_then(|client| {
client.identify().and(Ok(client)) client.identify().and(Ok(client))
}).and_then(|client| { }).and_then(|client| {
reactor.register_client_with_handler(client, process_msg); reactor.register_client_with_handler(client, process_msg);

View file

@ -7,8 +7,8 @@ fn main() {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]), alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]),
server: Some("irc.fyrechat.net".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default() ..Default::default()
}; };

View file

@ -6,8 +6,8 @@ use irc::client::prelude::*;
fn main() { fn main() {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
use_ssl: Some(true), use_ssl: Some(true),
..Default::default() ..Default::default()
}; };

51
examples/tooter.rs Normal file
View file

@ -0,0 +1,51 @@
extern crate irc;
extern crate tokio_timer;
use std::default::Default;
use std::time::Duration;
use irc::client::prelude::*;
use irc::error::IrcError;
// NOTE: this example is a conversion of `tweeter.rs` to an asynchronous style with `IrcReactor`.
fn main() {
let config = Config {
nickname: Some("mastodon".to_owned()),
server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#rust-spam".to_owned()]),
..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();
// 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(())
});
// 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();
}

View file

@ -5,11 +5,12 @@ use std::thread;
use std::time::Duration; use std::time::Duration;
use irc::client::prelude::*; use irc::client::prelude::*;
// NOTE: you can find an asynchronous version of this example with `IrcReactor` in `tooter.rs`.
fn main() { fn main() {
let config = Config { let config = Config {
nickname: Some("pickles".to_owned()), nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()), server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default() ..Default::default()
}; };
let client = IrcClient::from_config(config).unwrap(); let client = IrcClient::from_config(config).unwrap();
@ -20,7 +21,7 @@ fn main() {
client2.stream().map(|m| print!("{}", m)).wait().count(); client2.stream().map(|m| print!("{}", m)).wait().count();
}); });
loop { loop {
client.send_privmsg("#irc-crate", "TWEET TWEET").unwrap(); client.send_privmsg("#rust-spam", "TWEET TWEET").unwrap();
thread::sleep(Duration::new(10, 0)); thread::sleep(Duration::new(10, 0));
} }
} }

View file

@ -14,10 +14,11 @@ travis-ci = { repository = "aatxe/irc" }
[features] [features]
default = ["tokio"] default = ["tokio"]
tokio = ["tokio-io", "bytes"] tokio = ["tokio-codec", "tokio-io", "bytes"]
[dependencies] [dependencies]
bytes = { version = "0.4", optional = true } bytes = { version = "0.4", optional = true }
encoding = "0.2" encoding = "0.2"
failure = "0.1" failure = "0.1"
tokio-codec = { version = "0.1", optional = true }
tokio-io = { version = "0.1", optional = true } tokio-io = { version = "0.1", optional = true }

View file

@ -1,163 +1,188 @@
//! An extension trait that provides the ability to strip IRC colors from a string //! An extension trait that provides the ability to strip IRC colors from a string
use std::borrow::Cow; use std::borrow::Cow;
#[derive(PartialEq)]
enum ParserState { enum ParserState {
Text, Text,
ColorCode, ColorCode,
Foreground1, Foreground1(char),
Foreground2, Foreground2,
Comma, Comma,
Background1, Background1(char),
} }
struct Parser { struct Parser {
state: ParserState, state: ParserState,
} }
/// An extension trait giving strings a function to strip IRC colors /// An extension trait giving strings a function to strip IRC colors
pub trait FormattedStringExt { pub trait FormattedStringExt<'a> {
/// Returns true if the string contains color, bold, underline or italics /// Returns true if the string contains color, bold, underline or italics
fn is_formatted(&self) -> bool; fn is_formatted(&self) -> bool;
/// Returns the string with all color, bold, underline and italics stripped /// Returns the string with all color, bold, underline and italics stripped
fn strip_formatting(&self) -> Cow<str>; fn strip_formatting(self) -> Cow<'a, str>;
} }
const FORMAT_CHARACTERS: &[char] = &[
'\x02', // bold
'\x1F', // underline
'\x16', // reverse
'\x0F', // normal
'\x03', // color
];
impl FormattedStringExt for str { impl<'a> FormattedStringExt<'a> for &'a str {
fn is_formatted(&self) -> bool { fn is_formatted(&self) -> bool {
self.contains('\x02') || // bold self.contains(FORMAT_CHARACTERS)
self.contains('\x1F') || // underline
self.contains('\x16') || // reverse
self.contains('\x0F') || // normal
self.contains('\x03') // color
} }
fn strip_formatting(&self) -> Cow<str> { fn strip_formatting(self) -> Cow<'a, str> {
let mut parser = Parser { if !self.is_formatted() {
return Cow::Borrowed(self);
}
let mut s = String::from(self);
strip_formatting(&mut s);
Cow::Owned(s)
}
}
fn strip_formatting(buf: &mut String) {
let mut parser = Parser::new();
buf.retain(|cur| parser.next(cur));
}
impl Parser {
fn new() -> Self {
Parser {
state: ParserState::Text, state: ParserState::Text,
}; }
let mut prev: char = '\x00'; }
let result: Cow<str> = self
.chars() fn next(&mut self, cur: char) -> bool {
.filter(move |cur| { use self::ParserState::*;
let result = match parser.state { match self.state {
ParserState::Text | ParserState::Foreground1 | ParserState::Foreground2 if *cur == '\x03' => { Text | Foreground1(_) | Foreground2 if cur == '\x03' => {
parser.state = ParserState::ColorCode; self.state = ColorCode;
false false
}, }
ParserState::Text => !['\x02', '\x1F', '\x16', '\x0F'].contains(cur), Text => {
ParserState::ColorCode if (*cur).is_digit(10) => { !FORMAT_CHARACTERS.contains(&cur)
parser.state = ParserState::Foreground1; }
ColorCode if cur.is_digit(10) => {
self.state = Foreground1(cur);
false false
}, }
ParserState::Foreground1 if (*cur).is_digit(6) => { Foreground1('1') if cur.is_digit(6) => {
// can only consume another digit if previous char was 1. // can only consume another digit if previous char was 1.
if (prev) == '1' { self.state = Foreground2;
parser.state = ParserState::Foreground2;
false false
} else { }
parser.state = ParserState::Text; Foreground1(_) if cur.is_digit(6) => {
self.state = Text;
true true
} }
}, Foreground1(_) if cur == ',' => {
ParserState::Foreground1 if *cur == ',' => { self.state = Comma;
parser.state = ParserState::Comma;
false false
},
ParserState::Foreground2 if *cur == ',' => {
parser.state = ParserState::Comma;
false
},
ParserState::Comma if ((*cur).is_digit(10)) => {
parser.state = ParserState::Background1;
false
},
ParserState::Background1 if (*cur).is_digit(6) => {
// can only consume another digit if previous char was 1.
parser.state = ParserState::Text;
if (prev) == '1' {
false
} else {
true
} }
Foreground2 if cur == ',' => {
self.state = Comma;
false
}
Comma if (cur.is_digit(10)) => {
self.state = Background1(cur);
false
}
Background1(prev) if cur.is_digit(6) => {
// can only consume another digit if previous char was 1.
self.state = Text;
prev != '1'
} }
_ => { _ => {
parser.state = ParserState::Text; self.state = Text;
true true
} }
}; }
prev = *cur; }
return result
})
.collect();
result
} }
impl FormattedStringExt<'static> for String {
}
impl FormattedStringExt for String {
fn is_formatted(&self) -> bool { fn is_formatted(&self) -> bool {
(&self[..]).is_formatted() self.as_str().is_formatted()
} }
fn strip_formatting(&self) -> Cow<str> { fn strip_formatting(mut self) -> Cow<'static, str> {
(&self[..]).strip_formatting() if !self.is_formatted() {
return Cow::Owned(self);
}
strip_formatting(&mut self);
Cow::Owned(self)
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::FormattedStringExt; use std::borrow::Cow;
use colors::FormattedStringExt;
macro_rules! test_formatted_string_ext {
{ $( $name:ident ( $($line:tt)* ), )* } => {
$(
mod $name {
use super::*;
test_formatted_string_ext!(@ $($line)*);
}
)*
};
(@ $text:expr, should stripped into $expected:expr) => {
#[test] #[test]
fn test_strip_bold() { fn test_formatted() {
assert_eq!("l\x02ol".strip_formatting(), "lol"); assert!($text.is_formatted());
}
#[test]
fn test_strip() {
assert_eq!($text.strip_formatting(), $expected);
}
};
(@ $text:expr, is not formatted) => {
#[test]
fn test_formatted() {
assert!(!$text.is_formatted());
}
#[test]
fn test_strip() {
assert_eq!($text.strip_formatting(), $text);
}
}
}
test_formatted_string_ext! {
blank("", is not formatted),
blank2(" ", is not formatted),
blank3("\t\r\n", is not formatted),
bold("l\x02ol", should stripped into "lol"),
bold_from_string(String::from("l\x02ol"), should stripped into "lol"),
bold_hangul("우왕\x02", should stripped into "우왕굳"),
fg_color("l\x033ol", should stripped into "lol"),
fg_color2("l\x0312ol", should stripped into "lol"),
fg_bg_11("l\x031,2ol", should stripped into "lol"),
fg_bg_21("l\x0312,3ol", should stripped into "lol"),
fg_bg_12("l\x031,12ol", should stripped into "lol"),
fg_bg_22("l\x0312,13ol", should stripped into "lol"),
string_with_multiple_colors("hoo\x034r\x033a\x0312y", should stripped into "hooray"),
string_with_digit_after_color("\x0344\x0355\x0366", should stripped into "456"),
string_with_multiple_2digit_colors("hoo\x0310r\x0311a\x0312y", should stripped into "hooray"),
string_with_digit_after_2digit_color("\x031212\x031111\x031010", should stripped into "121110"),
thinking("🤔...", is not formatted),
unformatted("a plain text", is not formatted),
} }
#[test] #[test]
fn test_strip_fg_color() { fn test_strip_no_allocation_for_unformatted_text() {
assert_eq!("l\x033ol".strip_formatting(), "lol"); if let Cow::Borrowed(formatted) = "plain text".strip_formatting() {
} assert_eq!(formatted, "plain text");
} else {
#[test] panic!("allocation detected");
fn test_strip_fg_color2() { }
assert_eq!("l\x0312ol".strip_formatting(), "lol");
}
#[test]
fn test_strip_fg_bg_11() {
assert_eq!("l\x031,2ol".strip_formatting(), "lol");
}
#[test]
fn test_strip_fg_bg_21() {
assert_eq!("l\x0312,3ol".strip_formatting(), "lol");
}
#[test]
fn test_strip_fg_bg_12() {
assert_eq!("l\x031,12ol".strip_formatting(), "lol");
}
#[test]
fn test_strip_fg_bg_22() {
assert_eq!("l\x0312,13ol".strip_formatting(), "lol");
}
#[test]
fn test_strip_string_with_multiple_colors() {
assert_eq!("hoo\x034r\x033a\x0312y".strip_formatting(), "hooray");
}
#[test]
fn test_strip_string_with_digit_after_color() {
assert_eq!("\x0344\x0355\x0366".strip_formatting(), "456");
}
#[test]
fn test_strip_string_with_multiple_2digit_colors() {
assert_eq!("hoo\x0310r\x0311a\x0312y".strip_formatting(), "hooray");
}
#[test]
fn test_strip_string_with_digit_after_2digit_color() {
assert_eq!("\x031212\x031111\x031010".strip_formatting(), "121110");
} }
} }

View file

@ -1,5 +1,4 @@
//! Enumeration of all available client commands. //! Enumeration of all available client commands.
use std::ascii::AsciiExt;
use std::str::FromStr; use std::str::FromStr;
use error::MessageParseError; use error::MessageParseError;
@ -51,8 +50,32 @@ pub enum Command {
// 3.3 Sending messages // 3.3 Sending messages
/// PRIVMSG msgtarget :message /// PRIVMSG msgtarget :message
///
/// ## Responding to a `PRIVMSG`
///
/// When responding to a message, it is not sufficient to simply copy the message target
/// (msgtarget). This will work just fine for responding to messages in channels where the
/// target is the same for all participants. However, when the message is sent directly to a
/// user, this target will be that client's username, and responding to that same target will
/// actually mean sending itself a response. In such a case, you should instead respond to the
/// user sending the message as specified in the message prefix. Since this is a common
/// pattern, there is a utility function
/// [`Message::response_target`](../message/struct.Message.html#method.response_target)
/// which is used for this exact purpose.
PRIVMSG(String, String), PRIVMSG(String, String),
/// NOTICE msgtarget :message /// NOTICE msgtarget :message
///
/// ## Responding to a `NOTICE`
///
/// When responding to a notice, it is not sufficient to simply copy the message target
/// (msgtarget). This will work just fine for responding to messages in channels where the
/// target is the same for all participants. However, when the message is sent directly to a
/// user, this target will be that client's username, and responding to that same target will
/// actually mean sending itself a response. In such a case, you should instead respond to the
/// user sending the message as specified in the message prefix. Since this is a common
/// pattern, there is a utility function
/// [`Message::response_target`](../message/struct.Message.html#method.response_target)
/// which is used for this exact purpose.
NOTICE(String, String), NOTICE(String, String),
// 3.4 Server queries and commands // 3.4 Server queries and commands

View file

@ -1,6 +1,6 @@
//! Implementation of IRC codec for Tokio. //! Implementation of IRC codec for Tokio.
use bytes::BytesMut; use bytes::BytesMut;
use tokio_io::codec::{Decoder, Encoder}; use tokio_codec::{Decoder, Encoder};
use error; use error;
use line::LineCodec; use line::LineCodec;

View file

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

View file

@ -12,13 +12,14 @@ use error;
/// A line-based codec parameterized by an encoding. /// A line-based codec parameterized by an encoding.
pub struct LineCodec { pub struct LineCodec {
encoding: EncodingRef, encoding: EncodingRef,
next_index: usize,
} }
impl LineCodec { impl LineCodec {
/// Creates a new instance of LineCodec from the specified encoding. /// Creates a new instance of LineCodec from the specified encoding.
pub fn new(label: &str) -> error::Result<LineCodec> { pub fn new(label: &str) -> error::Result<LineCodec> {
encoding_from_whatwg_label(label) encoding_from_whatwg_label(label)
.map(|enc| LineCodec { encoding: enc }) .map(|enc| LineCodec { encoding: enc, next_index: 0 })
.ok_or_else(|| io::Error::new( .ok_or_else(|| io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
&format!("Attempted to use unknown codec {}.", label)[..], &format!("Attempted to use unknown codec {}.", label)[..],
@ -31,9 +32,12 @@ impl Decoder for LineCodec {
type Error = error::ProtocolError; type Error = error::ProtocolError;
fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<String>> { fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<String>> {
if let Some(n) = src.as_ref().iter().position(|b| *b == b'\n') { if let Some(offset) = src[self.next_index..].iter().position(|b| *b == b'\n') {
// Remove the next frame from the buffer. // Remove the next frame from the buffer.
let line = src.split_to(n + 1); let line = src.split_to(self.next_index + offset + 1);
// Set the search start index back to 0 since we found a newline.
self.next_index = 0;
// Decode the line using the codec's encoding. // Decode the line using the codec's encoding.
match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) { match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) {
@ -46,6 +50,9 @@ impl Decoder for LineCodec {
), ),
} }
} else { } else {
// Set the search start index to the current length since we know that none of the
// characters we've already looked at are newlines.
self.next_index = src.len();
Ok(None) Ok(None)
} }
} }

View file

@ -5,13 +5,13 @@ use std::io::Read;
use encoding::EncoderTrap; use encoding::EncoderTrap;
use encoding::label::encoding_from_whatwg_label; use encoding::label::encoding_from_whatwg_label;
use futures::future;
use futures::{Async, Poll, Future, Sink, StartSend, Stream}; use futures::{Async, Poll, Future, Sink, StartSend, Stream};
use native_tls::{Certificate, TlsConnector, Pkcs12}; use native_tls::{Certificate, TlsConnector, Identity};
use tokio::net::{ConnectFuture, TcpStream}; use tokio_codec::Decoder;
use tokio_io::AsyncRead; use tokio_core::reactor::Handle;
use tokio_core::net::{TcpStream, TcpStreamNew};
use tokio_mockstream::MockStream; use tokio_mockstream::MockStream;
use tokio_tls::{TlsConnectorExt, TlsStream}; use tokio_tls::{self, TlsStream};
use error; use error;
use client::data::Config; use client::data::Config;
@ -42,19 +42,20 @@ impl fmt::Debug for Connection {
} }
} }
/// A convenient type alias representing the `TlsStream` future.
type TlsFuture = Box<Future<Error = error::IrcError, Item = TlsStream<TcpStream>> + Send>; type TlsFuture = Box<Future<Error = error::IrcError, Item = TlsStream<TcpStream>> + Send>;
/// A future representing an eventual `Connection`. /// A future representing an eventual `Connection`.
pub enum ConnectionFuture { pub enum ConnectionFuture<'a> {
#[doc(hidden)] #[doc(hidden)]
Unsecured(Config, ConnectFuture), Unsecured(&'a Config, TcpStreamNew),
#[doc(hidden)] #[doc(hidden)]
Secured(Config, TlsFuture), Secured(&'a Config, TlsFuture),
#[doc(hidden)] #[doc(hidden)]
Mock(Config), Mock(&'a Config),
} }
impl fmt::Debug for ConnectionFuture { impl<'a> fmt::Debug for ConnectionFuture<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!( write!(
f, f,
@ -65,33 +66,35 @@ impl fmt::Debug for ConnectionFuture {
ConnectionFuture::Mock(_) => "ConnectionFuture::Mock", ConnectionFuture::Mock(_) => "ConnectionFuture::Mock",
}, },
match *self { match *self {
ConnectionFuture::Unsecured(ref cfg, _) | ConnectionFuture::Unsecured(cfg, _) |
ConnectionFuture::Secured(ref cfg, _) | ConnectionFuture::Secured(cfg, _) |
ConnectionFuture::Mock(ref cfg) => cfg, ConnectionFuture::Mock(cfg) => cfg,
} }
) )
} }
} }
impl Future for ConnectionFuture { impl<'a> Future for ConnectionFuture<'a> {
type Item = Connection; type Item = Connection;
type Error = error::IrcError; type Error = error::IrcError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
match *self { match *self {
ConnectionFuture::Unsecured(ref config, ref mut inner) => { ConnectionFuture::Unsecured(config, ref mut inner) => {
let framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?); let stream = try_ready!(inner.poll());
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = IrcTransport::new(config, framed); let transport = IrcTransport::new(config, framed);
Ok(Async::Ready(Connection::Unsecured(transport))) Ok(Async::Ready(Connection::Unsecured(transport)))
} }
ConnectionFuture::Secured(ref config, ref mut inner) => { ConnectionFuture::Secured(config, ref mut inner) => {
let framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?); let stream = try_ready!(inner.poll());
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = IrcTransport::new(config, framed); let transport = IrcTransport::new(config, framed);
Ok(Async::Ready(Connection::Secured(transport))) Ok(Async::Ready(Connection::Secured(transport)))
} }
ConnectionFuture::Mock(ref config) => { ConnectionFuture::Mock(config) => {
let enc: error::Result<_> = encoding_from_whatwg_label( let enc: error::Result<_> = encoding_from_whatwg_label(
config.encoding() config.encoding()
).ok_or_else(|| error::IrcError::UnknownCodec { ).ok_or_else(|| error::IrcError::UnknownCodec {
@ -108,7 +111,8 @@ impl Future for ConnectionFuture {
}) })
}; };
let framed = MockStream::new(&initial?).framed(IrcCodec::new(config.encoding())?); let stream = MockStream::new(&initial?);
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = IrcTransport::new(config, framed); let transport = IrcTransport::new(config, framed);
Ok(Async::Ready(Connection::Mock(Logged::wrap(transport)))) Ok(Async::Ready(Connection::Mock(Logged::wrap(transport))))
@ -118,56 +122,46 @@ impl Future for ConnectionFuture {
} }
impl Connection { impl Connection {
/// Creates a future yielding a new `Connection` using the specified `Config`. /// Creates a new `Connection` using the specified `Config` and `Handle`.
pub fn new(config: Config) -> impl Future<Item = Connection, Error = error::IrcError> { pub fn new<'a>(config: &'a Config, handle: &Handle) -> error::Result<ConnectionFuture<'a>> {
future::lazy(move || Connection::new_inner(config))
.and_then(|connection_future| connection_future)
}
fn new_inner(config: Config) -> error::Result<ConnectionFuture> {
if config.use_mock_connection() { if config.use_mock_connection() {
Ok(ConnectionFuture::Mock(config)) Ok(ConnectionFuture::Mock(config))
} else if config.use_ssl() { } else if config.use_ssl() {
let domain = format!("{}", config.server()?); let domain = format!("{}", config.server()?);
info!("Connecting via SSL to {}.", domain); info!("Connecting via SSL to {}.", domain);
let mut builder = TlsConnector::builder()?; let mut builder = TlsConnector::builder();
if let Some(cert_path) = config.cert_path() { if let Some(cert_path) = config.cert_path() {
let mut file = File::open(cert_path)?; let mut file = File::open(cert_path)?;
let mut cert_data = vec![]; let mut cert_data = vec![];
file.read_to_end(&mut cert_data)?; file.read_to_end(&mut cert_data)?;
let cert = Certificate::from_der(&cert_data)?; let cert = Certificate::from_der(&cert_data)?;
builder.add_root_certificate(cert)?; builder.add_root_certificate(cert);
info!("Added {} to trusted certificates.", cert_path); info!("Added {} to trusted certificates.", cert_path);
} }
if let Some(client_cert_path) = config.client_cert_path() { if let Some(client_cert_path) = config.client_cert_path() {
let client_cert_pass = config.client_cert_pass(); let client_cert_pass = config.client_cert_pass();
let mut file = File::open(client_cert_path)?; let mut file = File::open(client_cert_path)?;
let mut client_cert_data = vec![]; let mut client_cert_data = vec![];
file.read_to_end(&mut client_cert_data)?; file.read_to_end(&mut client_cert_data)?;
let pkcs12_archive = Pkcs12::from_der(&client_cert_data, &client_cert_pass)?; let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?;
builder.identity(pkcs12_archive)?; builder.identity(pkcs12_archive);
info!("Using {} for client certificate authentication.", client_cert_path); info!("Using {} for client certificate authentication.", client_cert_path);
} }
let connector: tokio_tls::TlsConnector = builder.build()?.into();
let connector = builder.build()?; let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle).map_err(|e| {
let socket_addr = config.socket_addr()?;
let stream = TcpStream::connect(&socket_addr).map_err(|e| {
let res: error::IrcError = e.into(); let res: error::IrcError = e.into();
res res
}).and_then(move |socket| { }).and_then(move |socket| {
connector.connect_async(&domain, socket).map_err(|e| e.into()) connector.connect(&domain, socket).map_err(
}); |e| e.into(),
)
Ok(ConnectionFuture::Secured(config, Box::new(stream))) }));
Ok(ConnectionFuture::Secured(config, stream))
} else { } else {
info!("Connecting to {}.", config.server()?); info!("Connecting to {}.", config.server()?);
let socket_addr = config.socket_addr()?;
Ok(ConnectionFuture::Unsecured( Ok(ConnectionFuture::Unsecured(
config, config,
TcpStream::connect(&socket_addr), TcpStream::connect(&config.socket_addr()?, handle),
)) ))
} }
} }

View file

@ -37,7 +37,7 @@
//! # client.identify().unwrap(); //! # client.identify().unwrap();
//! client.for_each_incoming(|irc_msg| { //! client.for_each_incoming(|irc_msg| {
//! if let Command::PRIVMSG(channel, message) = irc_msg.command { //! if let Command::PRIVMSG(channel, message) = irc_msg.command {
//! if message.contains(&*client.current_nickname()) { //! if message.contains(client.current_nickname()) {
//! client.send_privmsg(&channel, "beep boop").unwrap(); //! client.send_privmsg(&channel, "beep boop").unwrap();
//! } //! }
//! } //! }
@ -45,11 +45,9 @@
//! # } //! # }
//! ``` //! ```
#[cfg(feature = "ctcp")]
use std::ascii::AsciiExt;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard}; use std::sync::{Arc, Mutex, RwLock};
use std::thread; use std::thread;
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
@ -58,11 +56,11 @@ use futures::{Async, Poll, Future, Sink, Stream};
use futures::stream::SplitStream; use futures::stream::SplitStream;
use futures::sync::mpsc; use futures::sync::mpsc;
use futures::sync::oneshot; use futures::sync::oneshot;
use futures::sync::mpsc::UnboundedSender; use futures::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use tokio_core::reactor::Core; use tokio_core::reactor::{Core, Handle};
use error; use error;
use client::conn::Connection; use client::conn::{Connection, ConnectionFuture};
use client::data::{Config, User}; use client::data::{Config, User};
use client::ext::ClientExt; use client::ext::ClientExt;
use client::transport::LogView; use client::transport::LogView;
@ -94,7 +92,7 @@ pub mod transport;
/// # client.identify().unwrap(); /// # client.identify().unwrap();
/// client.stream().for_each_incoming(|irc_msg| { /// client.stream().for_each_incoming(|irc_msg| {
/// match irc_msg.command { /// match irc_msg.command {
/// Command::PRIVMSG(channel, message) => if message.contains(&*client.current_nickname()) { /// Command::PRIVMSG(channel, message) => if message.contains(client.current_nickname()) {
/// client.send_privmsg(&channel, "beep boop").unwrap(); /// client.send_privmsg(&channel, "beep boop").unwrap();
/// } /// }
/// _ => () /// _ => ()
@ -159,7 +157,7 @@ pub trait Client {
/// # client.identify().unwrap(); /// # client.identify().unwrap();
/// client.for_each_incoming(|irc_msg| { /// client.for_each_incoming(|irc_msg| {
/// if let Command::PRIVMSG(channel, message) = irc_msg.command { /// if let Command::PRIVMSG(channel, message) = irc_msg.command {
/// if message.contains(&*client.current_nickname()) { /// if message.contains(client.current_nickname()) {
/// client.send_privmsg(&channel, "beep boop").unwrap(); /// client.send_privmsg(&channel, "beep boop").unwrap();
/// } /// }
/// } /// }
@ -223,29 +221,17 @@ impl Stream for ClientStream {
} }
/// Thread-safe internal state for an IRC server connection. /// Thread-safe internal state for an IRC server connection.
///
/// Anything that should be synchronized across threads should be stuffed here. As `IrcClient` will
/// hold a single shared instance of `ClientState` using an `Arc`.
#[derive(Debug)] #[derive(Debug)]
struct ClientState { struct ClientState {
/// The configuration used with this connection. /// The configuration used with this connection.
config: Config, config: Config,
/// A thread-safe map of channels to the list of users in them. This is used to implement /// A thread-safe map of channels to the list of users in them.
/// the user tracking for each channel.
chanlists: Mutex<HashMap<String, Vec<User>>>, chanlists: Mutex<HashMap<String, Vec<User>>>,
/// A thread-safe index into `config.alt_nicks` to handle alternative nickname usage. /// A thread-safe index to track the current alternative nickname being used.
alt_nick_index: RwLock<usize>, alt_nick_index: RwLock<usize>,
/// The current nickname in use by this client, which may differ from the one implied by /// A thread-safe internal IRC stream used for the reading API.
/// `alt_nick_index`. This can be the case if, for example, a new `NICK` command is sent.
current_nickname: RwLock<String>,
/// The internal IRC stream used for the reading API. This stream can only be given out to one
/// thread, and from then on, the option will be empty. This may change in the future if we
/// split `Message` into an owned and borrowed variant (the latter being cheap to clone) since
/// we might then be able to forward the messages to many stream copies.
incoming: Mutex<Option<SplitStream<Connection>>>, incoming: Mutex<Option<SplitStream<Connection>>>,
/// The outgoing channel used for the sending API. This channel will send messages to the /// A thread-safe copy of the outgoing channel.
/// writing task (which could be on a different thread) that will handle the actual transmission
/// over the wire.
outgoing: UnboundedSender<Message>, outgoing: UnboundedSender<Message>,
} }
@ -256,8 +242,6 @@ impl<'a> Client for ClientState {
fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()> where Self: Sized { fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()> where Self: Sized {
let msg = msg.into(); let msg = msg.into();
// Before sending any messages to the writing task, we first process them for any special
// library-provided functionality.
self.handle_sent_message(&msg)?; self.handle_sent_message(&msg)?;
Ok(self.outgoing.unbounded_send(msg)?) Ok(self.outgoing.unbounded_send(msg)?)
} }
@ -303,29 +287,33 @@ impl ClientState {
incoming: SplitStream<Connection>, incoming: SplitStream<Connection>,
outgoing: UnboundedSender<Message>, outgoing: UnboundedSender<Message>,
config: Config, config: Config,
) -> error::Result<ClientState> { ) -> ClientState {
Ok(ClientState { ClientState {
config: config,
chanlists: Mutex::new(HashMap::new()), chanlists: Mutex::new(HashMap::new()),
alt_nick_index: RwLock::new(0), alt_nick_index: RwLock::new(0),
current_nickname: RwLock::new(config.nickname()?.to_owned()),
incoming: Mutex::new(Some(incoming)), incoming: Mutex::new(Some(incoming)),
outgoing, config, outgoing: outgoing,
}) }
} }
/// Gets the current nickname in use. /// Gets the current nickname in use.
fn current_nickname(&self) -> RwLockReadGuard<String> { fn current_nickname(&self) -> &str {
// This should never panic since we should never be poisoning the lock. let alt_nicks = self.config().alternate_nicknames();
self.current_nickname.read().unwrap() let index = self.alt_nick_index.read().unwrap();
match *index {
0 => self.config().nickname().expect(
"current_nickname should not be callable if nickname is not defined."
),
i => alt_nicks[i - 1],
}
} }
/// Handles sent messages internally for basic client functionality. /// Handles sent messages internally for basic client functionality.
fn handle_sent_message(&self, msg: &Message) -> error::Result<()> { fn handle_sent_message(&self, msg: &Message) -> error::Result<()> {
trace!("[SENT] {}", msg.to_string()); trace!("[SENT] {}", msg.to_string());
match msg.command { match msg.command {
// On sending a `PART`, we remove the channel from the channel listing.
PART(ref chan, _) => { PART(ref chan, _) => {
// This should never panic since we should never be poisoning the mutex.
let _ = self.chanlists.lock().unwrap().remove(chan); let _ = self.chanlists.lock().unwrap().remove(chan);
} }
_ => (), _ => (),
@ -337,37 +325,14 @@ impl ClientState {
fn handle_message(&self, msg: &Message) -> error::Result<()> { fn handle_message(&self, msg: &Message) -> error::Result<()> {
trace!("[RECV] {}", msg.to_string()); trace!("[RECV] {}", msg.to_string());
match msg.command { match msg.command {
// On a `JOIN` message, we add the user to the channel in the channel listing. This
// works on the assumption that the client will only see `JOIN` commands for channels it
// is a member of.
JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan), JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan),
// On a `PART` message, we remove the user from the channel in the channel listing. This
// works on the assumption that the client will only see `PART` commands for channels it
// is a member of.
PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan), PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan),
// On a `KICK` message, we remove the user from the channel in the channel listing. This
// works on the assumption that the client will only see `KICK` commands for channels it
// is a member of.
KICK(ref chan, ref user, _) => self.handle_part(user, chan), KICK(ref chan, ref user, _) => self.handle_part(user, chan),
// On a `QUIT` message, we remove the user from all channels in the channel listing.
QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")), QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")),
// On a `NICK` message, we might update the current nickname (if the `NICK` source is
// the client), and we always update channel tracking accordingly.
NICK(ref new_nick) => { NICK(ref new_nick) => {
self.handle_current_nick_change(msg.source_nickname().unwrap_or(""), new_nick);
self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick) self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick)
} }
// On a channel `MODE` message, the access level of a user might have changed (for
// example, in `MODE #pdgn +o awe`, the user `awe` is made an operator). Thus, we have
// to update the user instance in the channel listing accordingly.
ChannelMODE(ref chan, ref modes) => self.handle_mode(chan, modes), ChannelMODE(ref chan, ref modes) => self.handle_mode(chan, modes),
// On `PRIVMSG` commands, we pull out CTCP messages and process them accordingly.
PRIVMSG(ref target, ref body) => { PRIVMSG(ref target, ref body) => {
if body.starts_with('\u{001}') { if body.starts_with('\u{001}') {
let tokens: Vec<_> = { let tokens: Vec<_> = {
@ -385,15 +350,9 @@ impl ClientState {
} }
} }
} }
// When we receive `RPL_NAMREPLY`, we use it to populate the channel listing according
// to who is included in the reply.
Command::Response(Response::RPL_NAMREPLY, ref args, ref suffix) => { Command::Response(Response::RPL_NAMREPLY, ref args, ref suffix) => {
self.handle_namreply(args, suffix) self.handle_namreply(args, suffix)
} }
// After `RPL_ENDOFMOTD` or `ERR_NOMOTD`, the client is considered "ready" and is
// allowed to perform tasks such as joining channels or setting their usermode.
Command::Response(Response::RPL_ENDOFMOTD, _, _) | Command::Response(Response::RPL_ENDOFMOTD, _, _) |
Command::Response(Response::ERR_NOMOTD, _, _) => { Command::Response(Response::ERR_NOMOTD, _, _) => {
self.send_nick_password()?; self.send_nick_password()?;
@ -407,14 +366,13 @@ impl ClientState {
} }
} }
let joined_chans = self.chanlists.lock().unwrap(); let joined_chans = self.chanlists.lock().unwrap();
for chan in joined_chans.keys().filter(|x| !config_chans.contains(&x.as_str())) { for chan in joined_chans.keys().filter(
|x| !config_chans.contains(&x.as_str()),
)
{
self.send_join(chan)? self.send_join(chan)?
} }
} }
// When `ERR_NICKNAMEINUSE` or `ERR_ERRONEOUSNICKNAME` occurs, we use the alternative
// nicknames listed in the configuration to try a different `NICK`. Each time it fails,
// we move to the next alternative, until all alternatives are exhausted.
Command::Response(Response::ERR_NICKNAMEINUSE, _, _) | Command::Response(Response::ERR_NICKNAMEINUSE, _, _) |
Command::Response(Response::ERR_ERRONEOUSNICKNAME, _, _) => { Command::Response(Response::ERR_ERRONEOUSNICKNAME, _, _) => {
let alt_nicks = self.config().alternate_nicknames(); let alt_nicks = self.config().alternate_nicknames();
@ -426,15 +384,11 @@ impl ClientState {
*index += 1; *index += 1;
} }
} }
_ => (), _ => (),
} }
Ok(()) Ok(())
} }
/// If a password for the nickname is registered, send an identification command via `NICKSERV`.
/// This will also attempt to handle the necessary steps to replace an existing user with the
/// given nickname according to the ghost sequence specified in the configuration.
fn send_nick_password(&self) -> error::Result<()> { fn send_nick_password(&self) -> error::Result<()> {
if self.config().nick_password().is_empty() { if self.config().nick_password().is_empty() {
Ok(()) Ok(())
@ -458,7 +412,6 @@ impl ClientState {
} }
} }
/// If any user modes are specified in the configuration, this will send them to the server.
fn send_umodes(&self) -> error::Result<()> { fn send_umodes(&self) -> error::Result<()> {
if self.config().umodes().is_empty() { if self.config().umodes().is_empty() {
Ok(()) Ok(())
@ -520,17 +473,6 @@ impl ClientState {
} }
} }
/// If `old_nick` is the current nickname for this client, we'll update the current nickname to
/// `new_nick`. This should handle both user-initiated nickname changes _and_ server-intiated
/// ones.
fn handle_current_nick_change(&self, old_nick: &str, new_nick: &str) {
if old_nick.is_empty() || new_nick.is_empty() || old_nick != &*self.current_nickname() {
return;
}
let mut nick = self.current_nickname.write().unwrap();
*nick = new_nick.to_owned();
}
#[cfg(feature = "nochanlists")] #[cfg(feature = "nochanlists")]
fn handle_nick_change(&self, _: &str, _: &str) {} fn handle_nick_change(&self, _: &str, _: &str) {}
@ -655,7 +597,10 @@ impl Client for IrcClient {
&self.state.config &self.state.config
} }
fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()> where Self: Sized { fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()>
where
Self: Sized,
{
self.state.send(msg) self.state.send(msg)
} }
@ -748,39 +693,34 @@ impl IrcClient {
let cfg = config.clone(); let cfg = config.clone();
// This thread is run separately to writing outgoing messages to the wire, and hides the
// internal details of the `tokio` reactor from the programmer. However, by virtue of being
// a separate thread hidden from the programmer, its errors cannot be handled gracefully and
// will instead panic.
let _ = thread::spawn(move || { let _ = thread::spawn(move || {
let mut reactor = Core::new().unwrap(); let mut reactor = Core::new().unwrap();
let conn = reactor.run(Connection::new(cfg)).unwrap(); // Setting up internal processing stuffs.
let handle = reactor.handle();
let conn = reactor.run(Connection::new(&cfg, &handle).unwrap()).unwrap();
tx_view.send(conn.log_view()).unwrap(); tx_view.send(conn.log_view()).unwrap();
let (sink, stream) = conn.split(); let (sink, stream) = conn.split();
// Forward every message from the outgoing channel to the sink.
let outgoing_future = sink.send_all(rx_outgoing.map_err::<error::IrcError, _>(|_| { let outgoing_future = sink.send_all(rx_outgoing.map_err::<error::IrcError, _>(|_| {
unreachable!("futures::sync::mpsc::Receiver should never return Err"); unreachable!("futures::sync::mpsc::Receiver should never return Err");
})).map(|_| ()).map_err(|e| panic!("{}", e)); })).map(|_| ()).map_err(|e| panic!("{}", e));
// Send the stream half back to the original thread, to be stored in the client state. // Send the stream half back to the original thread.
tx_incoming.send(stream).unwrap(); tx_incoming.send(stream).unwrap();
// Run the future that writes outgoing messages to the wire forever or until we panic.
// This will block the thread.
reactor.run(outgoing_future).unwrap(); reactor.run(outgoing_future).unwrap();
}); });
Ok(IrcClient { Ok(IrcClient {
state: Arc::new(ClientState::new(rx_incoming.wait()?, tx_outgoing, config)?), state: Arc::new(ClientState::new(rx_incoming.wait()?, tx_outgoing, config)),
view: rx_view.wait()?, view: rx_view.wait()?,
}) })
} }
/// Creates a `Future` of an `IrcClient` from the specified configuration. /// Creates a `Future` of an `IrcClient` from the specified configuration and on the event loop
/// This can be used to set up a number of `IrcClients` on a single, /// corresponding to the given handle. This can be used to set up a number of `IrcClients` on a
/// shared event loop. It can also be used to take more control over execution and error /// single, shared event loop. It can also be used to take more control over execution and error
/// handling. Connection will not occur until the event loop is run. /// handling. Connection will not occur until the event loop is run.
/// ///
/// Proper usage requires familiarity with `tokio` and `futures`. You can find more information /// Proper usage requires familiarity with `tokio` and `futures`. You can find more information
@ -796,6 +736,7 @@ impl IrcClient {
/// # extern crate tokio_core; /// # extern crate tokio_core;
/// # use std::default::Default; /// # use std::default::Default;
/// # use irc::client::prelude::*; /// # use irc::client::prelude::*;
/// # use irc::client::PackedIrcClient;
/// # use irc::error; /// # use irc::error;
/// # use tokio_core::reactor::Core; /// # use tokio_core::reactor::Core;
/// # fn main() { /// # fn main() {
@ -805,9 +746,9 @@ impl IrcClient {
/// # .. Default::default() /// # .. Default::default()
/// # }; /// # };
/// let mut reactor = Core::new().unwrap(); /// let mut reactor = Core::new().unwrap();
/// let future = IrcClient::new_future(config); /// let future = IrcClient::new_future(reactor.handle(), &config).unwrap();
/// // immediate connection errors (like no internet) will turn up here... /// // immediate connection errors (like no internet) will turn up here...
/// let (client, future) = reactor.run(future).unwrap(); /// let PackedIrcClient(client, future) = reactor.run(future).unwrap();
/// // runtime errors (like disconnections and so forth) will turn up here... /// // runtime errors (like disconnections and so forth) will turn up here...
/// reactor.run(client.stream().for_each(move |irc_msg| { /// reactor.run(client.stream().for_each(move |irc_msg| {
/// // processing messages works like usual /// // processing messages works like usual
@ -816,34 +757,22 @@ impl IrcClient {
/// # } /// # }
/// # fn process_msg(server: &IrcClient, message: Message) -> error::Result<()> { Ok(()) } /// # fn process_msg(server: &IrcClient, message: Message) -> error::Result<()> { Ok(()) }
/// ``` /// ```
pub fn new_future(config: Config) -> impl Future< pub fn new_future(handle: Handle, config: &Config) -> error::Result<IrcClientFuture> {
Item = (IrcClient, impl Future<Item = (), Error = error::IrcError> + 'static),
Error = error::IrcError
> {
Connection::new(config.clone())
.and_then(move |connection| {
let (tx_outgoing, rx_outgoing) = mpsc::unbounded(); let (tx_outgoing, rx_outgoing) = mpsc::unbounded();
let log_view = connection.log_view();
let (sink, stream) = connection.split(); Ok(IrcClientFuture {
let outgoing_future = sink.send_all( conn: Connection::new(config, &handle)?,
rx_outgoing.map_err::<error::IrcError, _>(|()| { _handle: handle,
unreachable!("futures::sync::mpsc::Receiver should never return Err"); config: config,
}) tx_outgoing: Some(tx_outgoing),
).map(|_| ()); rx_outgoing: Some(rx_outgoing),
ClientState::new(stream, tx_outgoing, config).map(|state| {
let client = IrcClient {
state: Arc::new(state),
view: log_view,
};
(client, outgoing_future)
})
}) })
} }
/// Gets the current nickname in use. This may be the primary username set in the configuration, /// Gets the current nickname in use. This may be the primary username set in the configuration,
/// or it could be any of the alternative nicknames listed as well. As a result, this is the /// or it could be any of the alternative nicknames listed as well. As a result, this is the
/// preferred way to refer to the client's nickname. /// preferred way to refer to the client's nickname.
pub fn current_nickname(&self) -> RwLockReadGuard<String> { pub fn current_nickname(&self) -> &str {
self.state.current_nickname() self.state.current_nickname()
} }
@ -854,6 +783,56 @@ impl IrcClient {
} }
} }
/// A future representing the eventual creation of an `IrcClient`.
///
/// Interaction with this future relies on the `futures` API, but is only expected for more advanced
/// use cases. To learn more, you can view the documentation for the
/// [`futures`](https://docs.rs/futures/) crate, or the tutorials for
/// [`tokio`](https://tokio.rs/docs/getting-started/futures/). An easy to use abstraction that does
/// not require this knowledge is available via [`IrcReactors`](./reactor/struct.IrcReactor.html).
#[derive(Debug)]
pub struct IrcClientFuture<'a> {
conn: ConnectionFuture<'a>,
_handle: Handle,
config: &'a Config,
tx_outgoing: Option<UnboundedSender<Message>>,
rx_outgoing: Option<UnboundedReceiver<Message>>,
}
impl<'a> Future for IrcClientFuture<'a> {
type Item = PackedIrcClient;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let conn = try_ready!(self.conn.poll());
let view = conn.log_view();
let (sink, stream) = conn.split();
let outgoing_future = sink.send_all(
self.rx_outgoing.take().unwrap().map_err::<error::IrcError, _>(|()| {
unreachable!("futures::sync::mpsc::Receiver should never return Err");
})
).map(|_| ());
let server = IrcClient {
state: Arc::new(ClientState::new(
stream, self.tx_outgoing.take().unwrap(), self.config.clone()
)),
view: view,
};
Ok(Async::Ready(PackedIrcClient(server, Box::new(outgoing_future))))
}
}
/// An `IrcClient` packaged with a future that drives its message sending. In order for the client
/// to actually work properly, this future _must_ be running.
///
/// This type should only be used by advanced users who are familiar with the implementation of this
/// crate. An easy to use abstraction that does not require this knowledge is available via
/// [`IrcReactors`](./reactor/struct.IrcReactor.html).
pub struct PackedIrcClient(pub IrcClient, pub Box<Future<Item = (), Error = error::IrcError>>);
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::collections::HashMap; use std::collections::HashMap;
@ -1067,22 +1046,6 @@ mod test {
} }
} }
#[test]
fn current_nickname_tracking() {
let value = ":test!test@test NICK :t3st\r\n\
:t3st!test@test NICK :t35t\r\n";
let client = IrcClient::from_config(Config {
mock_initial_value: Some(value.to_owned()),
..test_config()
}).unwrap();
assert_eq!(&*client.current_nickname(), "test");
client.for_each_incoming(|message| {
println!("{:?}", message);
}).unwrap();
assert_eq!(&*client.current_nickname(), "t35t");
}
#[test] #[test]
fn send() { fn send() {
let client = IrcClient::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();

View file

@ -16,7 +16,7 @@
//! fn main() { //! fn main() {
//! let config = Config::default(); //! let config = Config::default();
//! let mut reactor = IrcReactor::new().unwrap(); //! let mut reactor = IrcReactor::new().unwrap();
//! let client = reactor.prepare_client_and_connect(config).unwrap(); //! let client = reactor.prepare_client_and_connect(&config).unwrap();
//! reactor.register_client_with_handler(client, process_msg); //! reactor.register_client_with_handler(client, process_msg);
//! reactor.run().unwrap(); //! reactor.run().unwrap();
//! } //! }
@ -28,7 +28,7 @@ use futures::future;
use tokio_core::reactor::{Core, Handle}; use tokio_core::reactor::{Core, Handle};
use client::data::Config; use client::data::Config;
use client::{IrcClient, Client}; use client::{IrcClient, IrcClientFuture, PackedIrcClient, Client};
use error; use error;
use proto::Message; use proto::Message;
@ -65,15 +65,12 @@ impl IrcReactor {
/// # fn main() { /// # fn main() {
/// # let config = Config::default(); /// # let config = Config::default();
/// let future_client = IrcReactor::new().and_then(|mut reactor| { /// let future_client = IrcReactor::new().and_then(|mut reactor| {
/// Ok(reactor.prepare_client(config)) /// reactor.prepare_client(&config)
/// }); /// });
/// # } /// # }
/// ``` /// ```
pub fn prepare_client(&mut self, config: Config) -> impl Future< pub fn prepare_client<'a>(&mut self, config: &'a Config) -> error::Result<IrcClientFuture<'a>> {
Item = (IrcClient, impl Future<Item = (), Error = error::IrcError> + 'static), IrcClient::new_future(self.inner_handle(), config)
Error = error::IrcError
> {
IrcClient::new_future(config)
} }
/// Runs an [`IrcClientFuture`](../struct.IrcClientFuture.html), such as one from /// Runs an [`IrcClientFuture`](../struct.IrcClientFuture.html), such as one from
@ -87,17 +84,14 @@ impl IrcReactor {
/// # fn main() { /// # fn main() {
/// # let config = Config::default(); /// # let config = Config::default();
/// let client = IrcReactor::new().and_then(|mut reactor| { /// let client = IrcReactor::new().and_then(|mut reactor| {
/// let future = reactor.prepare_client(config); /// reactor.prepare_client(&config).and_then(|future| {
/// reactor.connect_client(future) /// reactor.connect_client(future)
/// })
/// }); /// });
/// # } /// # }
/// ``` /// ```
pub fn connect_client<F, G>(&mut self, future: F) -> error::Result<IrcClient> pub fn connect_client(&mut self, future: IrcClientFuture) -> error::Result<IrcClient> {
where self.inner.run(future).map(|PackedIrcClient(client, future)| {
F: Future<Item = (IrcClient, G), Error = error::IrcError>,
G: Future<Item = (), Error = error::IrcError> + 'static,
{
self.inner.run(future).map(|(client, future)| {
self.register_future(future); self.register_future(future);
client client
}) })
@ -115,13 +109,12 @@ impl IrcReactor {
/// # fn main() { /// # fn main() {
/// # let config = Config::default(); /// # let config = Config::default();
/// let client = IrcReactor::new().and_then(|mut reactor| { /// let client = IrcReactor::new().and_then(|mut reactor| {
/// reactor.prepare_client_and_connect(config) /// reactor.prepare_client_and_connect(&config)
/// }); /// });
/// # } /// # }
/// ``` /// ```
pub fn prepare_client_and_connect(&mut self, config: Config) -> error::Result<IrcClient> { pub fn prepare_client_and_connect(&mut self, config: &Config) -> error::Result<IrcClient> {
let client_future = self.prepare_client(config); self.prepare_client(config).and_then(|future| self.connect_client(future))
self.connect_client(client_future)
} }
/// Registers the given client with the specified message handler. The reactor will store this /// Registers the given client with the specified message handler. The reactor will store this
@ -129,8 +122,6 @@ impl IrcReactor {
/// connection indefinitely (or until failure). As registration is consumed by `run`, subsequent /// connection indefinitely (or until failure). As registration is consumed by `run`, subsequent
/// calls to run will require new registration. /// calls to run will require new registration.
/// ///
/// **Note**: A client can only be registered once. Subsequent attempts will cause a panic.
///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// # extern crate irc; /// # extern crate irc;
@ -139,7 +130,7 @@ impl IrcReactor {
/// # fn main() { /// # fn main() {
/// # let config = Config::default(); /// # let config = Config::default();
/// let mut reactor = IrcReactor::new().unwrap(); /// let mut reactor = IrcReactor::new().unwrap();
/// let client = reactor.prepare_client_and_connect(config).unwrap(); /// let client = reactor.prepare_client_and_connect(&config).unwrap();
/// reactor.register_client_with_handler(client, |client, msg| { /// reactor.register_client_with_handler(client, |client, msg| {
/// // Message processing happens here. /// // Message processing happens here.
/// Ok(()) /// Ok(())
@ -185,7 +176,7 @@ impl IrcReactor {
/// # fn main() { /// # fn main() {
/// # let config = Config::default(); /// # let config = Config::default();
/// let mut reactor = IrcReactor::new().unwrap(); /// let mut reactor = IrcReactor::new().unwrap();
/// let client = reactor.prepare_client_and_connect(config).unwrap(); /// let client = reactor.prepare_client_and_connect(&config).unwrap();
/// reactor.register_client_with_handler(client, process_msg); /// reactor.register_client_with_handler(client, process_msg);
/// reactor.run().unwrap(); /// reactor.run().unwrap();
/// # } /// # }

View file

@ -7,8 +7,8 @@ use std::time::{Duration, Instant};
use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream}; use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream};
use chrono::prelude::*; use chrono::prelude::*;
use tokio_codec::Framed;
use tokio_io::{AsyncRead, AsyncWrite}; use tokio_io::{AsyncRead, AsyncWrite};
use tokio_io::codec::Framed;
use tokio_timer; use tokio_timer;
use tokio_timer::{Interval, Sleep, Timer}; use tokio_timer::{Interval, Sleep, Timer};

View file

@ -59,7 +59,7 @@ extern crate serde_derive;
extern crate serde_json; extern crate serde_json;
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
extern crate serde_yaml; extern crate serde_yaml;
extern crate tokio; extern crate tokio_codec;
extern crate tokio_core; extern crate tokio_core;
extern crate tokio_io; extern crate tokio_io;
extern crate tokio_mockstream; extern crate tokio_mockstream;