Merge pull request #110 from aatxe/develop

Finalize 0.13 release.
This commit is contained in:
Aaron Weiss 2018-01-28 04:37:09 +01:00 committed by GitHub
commit a1b271c167
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 2432 additions and 724 deletions

View file

@ -1,4 +1,4 @@
# Contributor Covenant Code of Conduct # Code of Conduct
## Our Pledge ## Our Pledge
@ -50,13 +50,15 @@ when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers. further defined and clarified by project maintainers. In general, non-maintainer
contributors will not be considered representing the project in public spaces
except when explicitly stating such.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [awe@pdgn.co](mailto:awe@pdgn.co). All reported by contacting the lead project maintainer at [awe@pdgn.co](mailto:awe@pdgn.co).
complaints will be reviewed and investigated and will result in a response that All complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident. obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately. Further details of specific enforcement policies may be posted separately.

View file

@ -1,6 +1,6 @@
[package] [package]
name = "irc" name = "irc"
version = "0.12.8" version = "0.13.0"
description = "A simple, thread-safe, and async-friendly library for IRC clients." description = "A simple, thread-safe, and async-friendly library for IRC clients."
authors = ["Aaron Weiss <awe@pdgn.co>"] authors = ["Aaron Weiss <awe@pdgn.co>"]
license = "MPL-2.0" license = "MPL-2.0"
@ -14,7 +14,7 @@ readme = "README.md"
travis-ci = { repository = "aatxe/irc" } travis-ci = { repository = "aatxe/irc" }
[features] [features]
default = ["ctcp", "json", "toml"] default = ["ctcp", "toml"]
ctcp = [] ctcp = []
nochanlists = [] nochanlists = []
json = ["serde_json"] json = ["serde_json"]
@ -25,7 +25,7 @@ bufstream = "0.1"
bytes = "0.4" bytes = "0.4"
chrono = "0.4" chrono = "0.4"
encoding = "0.2" encoding = "0.2"
error-chain = "0.10" failure = "0.1"
futures = "0.1" futures = "0.1"
log = "0.3" log = "0.3"
native-tls = "0.1" native-tls = "0.1"

View file

@ -1,6 +1,6 @@
# irc [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] [![Docs][doc-badge]][doc] [![Built with Spacemacs][bws]][sm] # irc [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] [![Docs][doc-badge]][doc] [![Built with Spacemacs][bws]][sm]
[ci-badge]: https://travis-ci.org/aatxe/irc.svg?branch=master [ci-badge]: https://travis-ci.org/aatxe/irc.svg?branch=stable
[ci]: https://travis-ci.org/aatxe/irc [ci]: https://travis-ci.org/aatxe/irc
[cr-badge]: https://img.shields.io/crates/v/irc.svg [cr-badge]: https://img.shields.io/crates/v/irc.svg
[cr]: https://crates.io/crates/irc [cr]: https://crates.io/crates/irc
@ -19,7 +19,7 @@ can be disabled accordingly.
## Getting Started ## ## Getting Started ##
To start using this library with cargo, you can simply add `irc = "0.12"` to your dependencies in To start using this library with cargo, you can simply add `irc = "0.13"` to your dependencies in
your Cargo.toml file. You'll likely want to take a look at some of the examples, as well as the your Cargo.toml file. You'll likely want to take a look at some of the examples, as well as the
documentation. You'll also be able to find below a small template to get a feel for the library. documentation. You'll also be able to find below a small template to get a feel for the library.
@ -74,12 +74,11 @@ fn main() {
Like the rest of the IRC crate, configuration is built with flexibility in mind. You can easily Like the rest of the IRC crate, configuration is built with flexibility in mind. You can easily
create `Config` objects programmatically and choose your own methods for handling any saving or create `Config` objects programmatically and choose your own methods for handling any saving or
loading of configuration required. However, for convenience, we've also included the option of loading of configuration required. However, for convenience, we've also included the option of
loading files with `serde` to write configurations. By default, we support JSON and TOML. As of loading files with `serde` to write configurations. The default configuration format is TOML,
0.12.4, TOML is the preferred configuration format. We have bundled a conversion tool as though there is optional support for JSON and YAML via the optional `json` and `yaml` features. All
`convertconf` in the examples. In a future version, we will likely disable JSON by default. the configuration fields are optional, and can thus be omitted, but a working configuration requires
Additionally, you can enable the optional `yaml` feature to get support for YAML as well. All the at least a `server` and `nickname`. You can find detailed explanations of the configuration format
configuration fields are optional, and thus any of them can be omitted (though, omitting a [here](https://docs.rs/irc/0.12.8/irc/client/data/config/struct.Config.html#fields).
nickname or server will cause the program to fail for obvious reasons).
Here's an example of a complete configuration in TOML: Here's an example of a complete configuration in TOML:
@ -130,5 +129,4 @@ tool should make it easy for users to migrate their old configurations to TOML.
Contributions to this library would be immensely appreciated. Prior to version 0.12.0, this Contributions to this library would be immensely appreciated. Prior to version 0.12.0, this
library was public domain. As of 0.12.0, this library is offered under the Mozilla Public License library was public domain. As of 0.12.0, this library is offered under the Mozilla Public License
2.0 whose text can be found in `LICENSE.md`. Fostering an inclusive community around `irc` is 2.0 whose text can be found in `LICENSE.md`. Fostering an inclusive community around `irc` is
important, and to that end, we've adopted the important, and to that end, we've adopted an explicit Code of Conduct found in `CODE_OF_CONDUCT.md`.
[Contributor Convenant](https://www.contributor-covenant.org).

View file

@ -16,7 +16,7 @@ fn main() {
let args: Vec<_> = env::args().collect(); let args: Vec<_> = env::args().collect();
match parse(&args) { match parse(&args) {
Ok(Some((ref input, ref output))) => { Ok(Some((ref input, ref output))) => {
let cfg = Config::load(input).unwrap(); let mut cfg = Config::load(input).unwrap();
cfg.save(output).unwrap(); cfg.save(output).unwrap();
println!("Converted {} to {}.", input, output); println!("Converted {} to {}.", input, output);
} }

View file

@ -1,8 +1,6 @@
extern crate futures;
extern crate irc; extern crate irc;
use std::default::Default; use std::default::Default;
use futures::stream::MergedItem;
use irc::error; use irc::error;
use irc::client::prelude::*; use irc::client::prelude::*;
@ -13,35 +11,38 @@ fn main() {
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default() ..Default::default()
}; };
let cfg2 = Config { let cfg2 = Config {
nickname: Some("pickles".to_owned()), nickname: Some("bananas".to_owned()),
server: Some("irc.pdgn.co".to_owned()), server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#irc-crate".to_owned()]),
use_ssl: Some(true),
..Default::default() ..Default::default()
}; };
let server1 = IrcServer::from_config(cfg1).unwrap(); let configs = vec![cfg1, cfg2];
let server2 = IrcServer::from_config(cfg2).unwrap();
server1.identify().unwrap();
server2.identify().unwrap();
server1.stream().merge(server2.stream()).for_each(|pair| match pair { let mut reactor = IrcReactor::new().unwrap();
MergedItem::First(message) => process_msg(&server1, message),
MergedItem::Second(message) => process_msg(&server2, message), for config in configs {
MergedItem::Both(msg1, msg2) => { // Immediate errors like failure to resolve the server's domain or to establish any connection will
process_msg(&server1, msg1).unwrap(); // manifest here in the result of prepare_client_and_connect.
process_msg(&server2, msg2) let client = reactor.prepare_client_and_connect(&config).unwrap();
} client.identify().unwrap();
}).wait().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);
}
// Runtime errors like a dropped connection will manifest here in the result of run.
reactor.run().unwrap();
} }
fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> {
print!("{}", message); print!("{}", message);
match message.command { match message.command {
Command::PRIVMSG(ref target, ref msg) => { Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") { if msg.contains("pickles") {
server.send_privmsg(target, "Hi!")?; client.send_privmsg(target, "Hi!")?;
} }
} }
_ => (), _ => (),

View file

@ -1,56 +0,0 @@
extern crate futures;
extern crate irc;
extern crate tokio_core;
use std::default::Default;
use futures::future;
use irc::error;
use irc::client::prelude::*;
use tokio_core::reactor::Core;
fn main() {
let cfg1 = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default()
};
let cfg2 = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]),
use_ssl: Some(true),
..Default::default()
};
let configs = vec![cfg1, cfg2];
// Create an event loop to run the multiple connections on.
let mut reactor = Core::new().unwrap();
let handle = reactor.handle();
for config in configs {
let server = IrcServer::from_config(config).unwrap();
server.identify().unwrap();
handle.spawn(server.stream().for_each(move |message| {
process_msg(&server, message)
}).map_err(|e| Err(e).unwrap()))
}
// You might instead want to join all the futures and run them directly.
reactor.run(future::empty::<(), ()>()).unwrap();
}
fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> {
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") {
server.send_privmsg(target, "Hi!")?;
}
}
_ => (),
}
Ok(())
}

View file

@ -1,58 +0,0 @@
extern crate futures;
extern crate irc;
extern crate tokio_core;
use std::default::Default;
use futures::future;
use irc::error;
use irc::client::prelude::*;
use tokio_core::reactor::Core;
fn main() {
let cfg1 = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default()
};
let cfg2 = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]),
use_ssl: Some(true),
..Default::default()
};
let configs = vec![cfg1, cfg2];
// Create an event loop to run the multiple connections on.
let mut reactor = Core::new().unwrap();
let processor_handle = reactor.handle();
for config in configs {
let handle = reactor.handle();
let server = reactor.run(IrcServer::new_future(handle, &config).unwrap()).unwrap();
server.identify().unwrap();
processor_handle.spawn(server.stream().for_each(move |message| {
process_msg(&server, message)
}).map_err(|e| Err(e).unwrap()))
}
// You might instead want to join all the futures and run them directly.
reactor.run(future::empty::<(), ()>()).unwrap();
}
fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> {
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") {
server.send_privmsg(target, "Hi!")?;
}
}
_ => (),
}
Ok(())
}

View file

@ -1,62 +0,0 @@
extern crate futures;
extern crate irc;
extern crate tokio_core;
use std::default::Default;
use futures::future;
use irc::error;
use irc::client::prelude::*;
use tokio_core::reactor::Core;
fn main() {
let cfg1 = Config {
nickname: Some("pickles1".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default()
};
let cfg2 = Config {
nickname: Some("pickles2".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default()
};
let configs = vec![cfg1, cfg2];
let (futures, mut reactor) = configs.iter().fold(
(vec![], Core::new().unwrap()),
|(mut acc, mut reactor), config| {
let handle = reactor.handle();
// First, we run the future representing the connection to the server.
// After this is complete, we have connected and can send and receive messages.
let server = reactor.run(IrcServer::new_future(handle, config).unwrap()).unwrap();
server.identify().unwrap();
// Add the future for processing messages from the current server to the accumulator.
acc.push(server.stream().for_each(move |message| {
process_msg(&server, message)
}));
// We then thread through the updated accumulator and the reactor.
(acc, reactor)
}
);
// Here, we join on all of the futures representing the message handling for each server.
reactor.run(future::join_all(futures)).unwrap();
}
fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> {
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") {
server.send_privmsg(target, "Hi!")?;
}
}
_ => (),
}
Ok(())
}

31
examples/reactor.rs Normal file
View file

@ -0,0 +1,31 @@
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.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".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();
}

57
examples/reconnector.rs Normal file
View file

@ -0,0 +1,57 @@
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.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default()
};
let cfg2 = Config {
nickname: Some("bananas".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".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).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

@ -15,29 +15,26 @@ fn main() {
..Default::default() ..Default::default()
}; };
let server = IrcServer::from_config(config).unwrap(); let client = IrcClient::from_config(config).unwrap();
server.identify().unwrap(); client.identify().unwrap();
server.for_each_incoming(|message| { client.for_each_incoming(|message| {
print!("{}", message); print!("{}", message);
match message.command { if let Command::PRIVMSG(ref target, ref msg) = message.command {
Command::PRIVMSG(ref target, ref msg) => { if msg.starts_with(client.current_nickname()) {
if msg.starts_with(server.current_nickname()) { let tokens: Vec<_> = msg.split(' ').collect();
let tokens: Vec<_> = msg.split(' ').collect(); if tokens.len() > 2 {
if tokens.len() > 2 { let n = tokens[0].len() + tokens[1].len() + 2;
let n = tokens[0].len() + tokens[1].len() + 2; if let Ok(count) = tokens[1].parse::<u8>() {
if let Ok(count) = tokens[1].parse::<u8>() { for _ in 0..count {
for _ in 0..count { client.send_privmsg(
server.send_privmsg( message.response_target().unwrap_or(target),
message.response_target().unwrap_or(target), &msg[n..]
&msg[n..] ).unwrap();
).unwrap();
}
} }
} }
} }
} }
_ => (),
} }
}).unwrap() }).unwrap()
} }

View file

@ -12,18 +12,15 @@ fn main() {
..Default::default() ..Default::default()
}; };
let server = IrcServer::from_config(config).unwrap(); let client = IrcClient::from_config(config).unwrap();
server.identify().unwrap(); client.identify().unwrap();
server.for_each_incoming(|message| { client.for_each_incoming(|message| {
print!("{}", message); print!("{}", message);
match message.command { if let Command::PRIVMSG(ref target, ref msg) = message.command {
Command::PRIVMSG(ref target, ref msg) => { if msg.contains("pickles") {
if msg.contains("pickles") { client.send_privmsg(target, "Hi!").unwrap();
server.send_privmsg(target, "Hi!").unwrap();
}
} }
_ => (),
} }
}).unwrap() }).unwrap();
} }

View file

@ -12,18 +12,15 @@ fn main() {
..Default::default() ..Default::default()
}; };
let server = IrcServer::from_config(config).unwrap(); let client = IrcClient::from_config(config).unwrap();
server.identify().unwrap(); client.identify().unwrap();
server.for_each_incoming(|message| { client.for_each_incoming(|message| {
print!("{}", message); print!("{}", message);
match message.command { if let Command::PRIVMSG(ref target, ref msg) = message.command {
Command::PRIVMSG(ref target, ref msg) => { if msg.contains("pickles") {
if msg.contains("pickles") { client.send_privmsg(target, "Hi!").unwrap();
server.send_privmsg(target, "Hi!").unwrap();
}
} }
_ => (),
} }
}).unwrap() }).unwrap();
} }

View file

@ -12,15 +12,15 @@ fn main() {
channels: Some(vec!["#irc-crate".to_owned()]), channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default() ..Default::default()
}; };
let server = IrcServer::from_config(config).unwrap(); let client = IrcClient::from_config(config).unwrap();
server.identify().unwrap(); client.identify().unwrap();
let server2 = server.clone(); let client2 = client.clone();
// Let's set up a loop that just prints the messages. // Let's set up a loop that just prints the messages.
thread::spawn(move || { thread::spawn(move || {
server2.stream().map(|m| print!("{}", m)).wait().count(); client2.stream().map(|m| print!("{}", m)).wait().count();
}); });
loop { loop {
server.send_privmsg("#irc-crate", "TWEET TWEET").unwrap(); client.send_privmsg("#irc-crate", "TWEET TWEET").unwrap();
thread::sleep(Duration::new(10, 0)); thread::sleep(Duration::new(10, 0));
} }
} }

View file

@ -1,3 +1,3 @@
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 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 "toml yaml" -- -i client_config.json -o client_config.toml cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.toml
cargo run --example convertconf --features "toml yaml" -- -i client_config.json -o client_config.yaml cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.yaml

View file

@ -1,6 +1,6 @@
//! A module providing IRC connections for use by `IrcServer`s. //! A module providing IRC connections for use by `IrcServer`s.
use std::fs::File; use std::fs::File;
use std::{fmt, io}; use std::fmt;
use std::io::Read; use std::io::Read;
use encoding::EncoderTrap; use encoding::EncoderTrap;
@ -43,7 +43,7 @@ impl fmt::Debug for Connection {
} }
/// A convenient type alias representing the `TlsStream` future. /// A convenient type alias representing the `TlsStream` future.
type TlsFuture = Box<Future<Error = error::Error, 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<'a> { pub enum ConnectionFuture<'a> {
@ -55,42 +55,58 @@ pub enum ConnectionFuture<'a> {
Mock(&'a Config), Mock(&'a Config),
} }
impl<'a> fmt::Debug for ConnectionFuture<'a> {
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(cfg, _) |
ConnectionFuture::Secured(cfg, _) |
ConnectionFuture::Mock(cfg) => cfg,
}
)
}
}
impl<'a> Future for ConnectionFuture<'a> { impl<'a> Future for ConnectionFuture<'a> {
type Item = Connection; type Item = Connection;
type Error = error::Error; 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 framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?);
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 framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?);
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(|| io::Error::new( ).ok_or_else(|| error::IrcError::UnknownCodec {
io::ErrorKind::InvalidInput, codec: config.encoding().to_owned(),
&format!("Attempted to use unknown codec {}.", config.encoding())[..], });
).into());
let encoding = enc?; let encoding = enc?;
let init_str = config.mock_initial_value(); let init_str = config.mock_initial_value();
let initial: error::Result<_> = { let initial: error::Result<_> = {
encoding.encode(init_str, EncoderTrap::Replace).map_err( encoding.encode(init_str, EncoderTrap::Replace).map_err(|data| {
|data| { error::IrcError::CodecFailed {
io::Error::new( codec: encoding.name(),
io::ErrorKind::InvalidInput, data: data.into_owned(),
&format!("Failed to encode {} as {}.", data, encoding.name())[..], }
).into() })
},
)
}; };
let framed = MockStream::new(&initial?).framed(IrcCodec::new(config.encoding())?); let framed = MockStream::new(&initial?).framed(IrcCodec::new(config.encoding())?);
@ -108,7 +124,7 @@ impl Connection {
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() {
@ -120,20 +136,17 @@ impl Connection {
info!("Added {} to trusted certificates.", cert_path); info!("Added {} to trusted certificates.", cert_path);
} }
let connector = builder.build()?; let connector = builder.build()?;
let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle) let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle).map_err(|e| {
.map_err(|e| { let res: error::IrcError = e.into();
let res: error::Error = e.into(); res
res }).and_then(move |socket| {
}) connector.connect_async(&domain, socket).map_err(
.and_then(move |socket| { |e| e.into(),
connector.connect_async(&domain, socket).map_err( )
|e| e.into(), }));
)
}
));
Ok(ConnectionFuture::Secured(config, stream)) Ok(ConnectionFuture::Secured(config, stream))
} else { } else {
info!("Connecting to {}.", config.server()); info!("Connecting to {}.", config.server()?);
Ok(ConnectionFuture::Unsecured( Ok(ConnectionFuture::Unsecured(
config, config,
TcpStream::connect(&config.socket_addr()?, handle), TcpStream::connect(&config.socket_addr()?, handle),
@ -153,7 +166,7 @@ impl Connection {
impl Stream for Connection { impl Stream for Connection {
type Item = Message; type Item = Message;
type Error = error::Error; type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
match *self { match *self {
@ -166,7 +179,7 @@ impl Stream for Connection {
impl Sink for Connection { impl Sink for Connection {
type SinkItem = Message; type SinkItem = Message;
type SinkError = error::Error; type SinkError = error::IrcError;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> { fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
match *self { match *self {

View file

@ -3,9 +3,8 @@ use std::borrow::ToOwned;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::{Error, ErrorKind};
use std::net::{SocketAddr, ToSocketAddrs}; use std::net::{SocketAddr, ToSocketAddrs};
use std::path::Path; use std::path::{Path, PathBuf};
#[cfg(feature = "json")] #[cfg(feature = "json")]
use serde_json; use serde_json;
@ -14,8 +13,10 @@ use serde_yaml;
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
use toml; use toml;
use error; #[cfg(feature = "toml")]
use error::{Result, ResultExt}; use error::TomlError;
use error::{ConfigError, Result};
use error::IrcError::InvalidConfig;
/// Configuration data. /// Configuration data.
#[derive(Clone, Deserialize, Serialize, Default, PartialEq, Debug)] #[derive(Clone, Deserialize, Serialize, Default, PartialEq, Debug)]
@ -88,9 +89,25 @@ pub struct Config {
pub channel_keys: Option<HashMap<String, String>>, pub channel_keys: Option<HashMap<String, String>>,
/// A map of additional options to be stored in config. /// A map of additional options to be stored in config.
pub options: Option<HashMap<String, String>>, pub options: Option<HashMap<String, String>>,
/// The path that this configuration was loaded from.
///
/// This should not be specified in any configuration. It will automatically be handled by the library.
pub path: Option<PathBuf>,
} }
impl Config { impl Config {
fn with_path<P: AsRef<Path>>(mut self, path: P) -> Config {
self.path = Some(path.as_ref().to_owned());
self
}
fn path(&self) -> String {
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 /// Loads a configuration from the desired path. This will use the file extension to detect
/// which format to parse the file as (json, toml, or yaml). Using each format requires having /// which format to parse the file as (json, toml, or yaml). Using each format requires having
/// its respective crate feature enabled. Only json is available by default. /// its respective crate feature enabled. Only json is available by default.
@ -99,155 +116,171 @@ impl Config {
let mut data = String::new(); let mut data = String::new();
file.read_to_string(&mut data)?; file.read_to_string(&mut data)?;
match path.as_ref().extension().and_then(|s| s.to_str()) { let res = match path.as_ref().extension().and_then(|s| s.to_str()) {
Some("json") => Config::load_json(&data), Some("json") => Config::load_json(&path, &data),
Some("toml") => Config::load_toml(&data), Some("toml") => Config::load_toml(&path, &data),
Some("yaml") | Some("yml") => Config::load_yaml(&data), Some("yaml") | Some("yml") => Config::load_yaml(&path, &data),
Some(ext) => Err(Error::new( Some(ext) => Err(InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
format!("Failed to decode configuration of unknown format {}", ext), cause: ConfigError::UnknownConfigFormat {
).into()), format: ext.to_owned(),
None => Err(Error::new( },
ErrorKind::InvalidInput, }),
"Failed to decode configuration of missing or non-unicode format.", None => Err(InvalidConfig {
).into()), path: path.as_ref().to_string_lossy().into_owned(),
} cause: ConfigError::MissingExtension,
}),
};
res.map(|config| {
config.with_path(path)
})
} }
#[cfg(feature = "json")] #[cfg(feature = "json")]
fn load_json(data: &str) -> Result<Config> { fn load_json<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
serde_json::from_str(&data[..]).chain_err(|| { serde_json::from_str(&data[..]).map_err(|e| {
let e: error::Error = Error::new( InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"Failed to decode JSON configuration file.", cause: ConfigError::InvalidJson(e),
).into(); }
e
}) })
} }
#[cfg(not(feature = "json"))] #[cfg(not(feature = "json"))]
fn load_json(_: &str) -> Result<Config> { fn load_json<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> {
Err(Error::new( Err(InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"JSON file decoding is disabled.", cause: ConfigError::ConfigFormatDisabled {
).into()) format: "JSON"
}
})
} }
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn load_toml(data: &str) -> Result<Config> { fn load_toml<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
toml::from_str(&data[..]).chain_err(|| { toml::from_str(&data[..]).map_err(|e| {
let e: error::Error = Error::new( InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"Failed to decode TOML configuration file.", cause: ConfigError::InvalidToml(TomlError::Read(e)),
).into(); }
e
}) })
} }
#[cfg(not(feature = "toml"))] #[cfg(not(feature = "toml"))]
fn load_toml(_: &str) -> Result<Config> { fn load_toml<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> {
Err(Error::new( Err(InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"TOML file decoding is disabled.", cause: ConfigError::ConfigFormatDisabled {
).into()) format: "TOML"
}
})
} }
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
fn load_yaml(data: &str) -> Result<Config> { fn load_yaml<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
serde_yaml::from_str(&data[..]).chain_err(|| { serde_yaml::from_str(&data[..]).map_err(|e| {
let e: error::Error = Error::new( InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"Failed to decode YAML configuration file.", cause: ConfigError::InvalidYaml(e),
).into(); }
e
}) })
} }
#[cfg(not(feature = "yaml"))] #[cfg(not(feature = "yaml"))]
fn load_yaml(_: &str) -> Result<Config> { fn load_yaml<P: AsRef<Path>>(path: &P, _: &str) -> Result<Config> {
Err(Error::new( Err(InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"YAML file decoding is disabled.", cause: ConfigError::ConfigFormatDisabled {
).into()) format: "YAML"
}
})
} }
/// Saves a configuration to the desired path. This will use the file extension to detect /// Saves a configuration to the desired path. This will use the file extension to detect
/// which format to parse the file as (json, toml, or yaml). Using each format requires having /// which format to parse the file as (json, toml, or yaml). Using each format requires having
/// its respective crate feature enabled. Only json is available by default. /// its respective crate feature enabled. Only json is available by default.
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> { pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let _ = self.path.take();
let mut file = File::create(&path)?; let mut file = File::create(&path)?;
let data = match path.as_ref().extension().and_then(|s| s.to_str()) { let data = match path.as_ref().extension().and_then(|s| s.to_str()) {
Some("json") => self.save_json()?, Some("json") => self.save_json(&path)?,
Some("toml") => self.save_toml()?, Some("toml") => self.save_toml(&path)?,
Some("yaml") | Some("yml") => self.save_yaml()?, Some("yaml") | Some("yml") => self.save_yaml(&path)?,
Some(ext) => return Err(Error::new( Some(ext) => return Err(InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
format!("Failed to encode configuration of unknown format {}", ext), cause: ConfigError::UnknownConfigFormat {
).into()), format: ext.to_owned(),
None => return Err(Error::new( },
ErrorKind::InvalidInput, }),
"Failed to encode configuration of missing or non-unicode format.", None => return Err(InvalidConfig {
).into()), path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::MissingExtension,
}),
}; };
file.write_all(data.as_bytes())?; file.write_all(data.as_bytes())?;
self.path = Some(path.as_ref().to_owned());
Ok(()) Ok(())
} }
#[cfg(feature = "json")] #[cfg(feature = "json")]
fn save_json(&self) -> Result<String> { fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
serde_json::to_string(self).chain_err(|| { serde_json::to_string(self).map_err(|e| {
let e: error::Error = Error::new( InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"Failed to encode JSON configuration file.", cause: ConfigError::InvalidJson(e),
).into(); }
e
}) })
} }
#[cfg(not(feature = "json"))] #[cfg(not(feature = "json"))]
fn save_json(&self) -> Result<String> { fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(Error::new( Err(InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"JSON file encoding is disabled.", cause: ConfigError::ConfigFormatDisabled {
).into()) format: "JSON"
}
})
} }
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn save_toml(&self) -> Result<String> { fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
toml::to_string(self).chain_err(|| { toml::to_string(self).map_err(|e| {
let e: error::Error = Error::new( InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"Failed to encode TOML configuration file.", cause: ConfigError::InvalidToml(TomlError::Write(e)),
).into(); }
e
}) })
} }
#[cfg(not(feature = "toml"))] #[cfg(not(feature = "toml"))]
fn save_toml(&self) -> Result<String> { fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(Error::new( Err(InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"TOML file encoding is disabled.", cause: ConfigError::ConfigFormatDisabled {
).into()) format: "TOML"
}
})
} }
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
fn save_yaml(&self) -> Result<String> { fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
serde_yaml::to_string(self).chain_err(|| { serde_yaml::to_string(self).map_err(|e| {
let e: error::Error = Error::new( InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"Failed to encode YAML configuration file.", cause: ConfigError::InvalidYaml(e),
).into(); }
e
}) })
} }
#[cfg(not(feature = "yaml"))] #[cfg(not(feature = "yaml"))]
fn save_yaml(&self) -> Result<String> { fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(Error::new( Err(InvalidConfig {
ErrorKind::InvalidInput, path: path.as_ref().to_string_lossy().into_owned(),
"YAML file encoding is disabled.", cause: ConfigError::ConfigFormatDisabled {
).into()) format: "YAML"
}
})
} }
/// Determines whether or not the nickname provided is the owner of the bot. /// Determines whether or not the nickname provided is the owner of the bot.
@ -259,9 +292,13 @@ impl Config {
} }
/// Gets the nickname specified in the configuration. /// Gets the nickname specified in the configuration.
/// This will panic if not specified. pub fn nickname(&self) -> Result<&str> {
pub fn nickname(&self) -> &str { self.nickname.as_ref().map(|s| &s[..]).ok_or_else(|| {
self.nickname.as_ref().map(|s| &s[..]).unwrap() InvalidConfig {
path: self.path(),
cause: ConfigError::NicknameNotSpecified,
}
})
} }
/// Gets the bot's nickserv password specified in the configuration. /// Gets the bot's nickserv password specified in the configuration.
@ -282,19 +319,23 @@ impl Config {
/// Gets the username specified in the configuration. /// Gets the username specified in the configuration.
/// This defaults to the user's nickname when not specified. /// This defaults to the user's nickname when not specified.
pub fn username(&self) -> &str { pub fn username(&self) -> &str {
self.username.as_ref().map_or(self.nickname(), |s| &s) self.username.as_ref().map_or(self.nickname().unwrap_or("user"), |s| &s)
} }
/// Gets the real name specified in the configuration. /// Gets the real name specified in the configuration.
/// This defaults to the user's nickname when not specified. /// This defaults to the user's nickname when not specified.
pub fn real_name(&self) -> &str { pub fn real_name(&self) -> &str {
self.realname.as_ref().map_or(self.nickname(), |s| &s) self.realname.as_ref().map_or(self.nickname().unwrap_or("irc"), |s| &s)
} }
/// Gets the address of the server specified in the configuration. /// Gets the address of the server specified in the configuration.
/// This panics when not specified. pub fn server(&self) -> Result<&str> {
pub fn server(&self) -> &str { self.server.as_ref().map(|s| &s[..]).ok_or_else(|| {
self.server.as_ref().map(|s| &s[..]).unwrap() InvalidConfig {
path: self.path(),
cause: ConfigError::ServerNotSpecified,
}
})
} }
/// Gets the port of the server specified in the configuration. /// Gets the port of the server specified in the configuration.
@ -310,7 +351,7 @@ impl Config {
/// Gets the server and port as a `SocketAddr`. /// Gets the server and port as a `SocketAddr`.
/// This panics when server is not specified or the address is malformed. /// This panics when server is not specified or the address is malformed.
pub fn socket_addr(&self) -> Result<SocketAddr> { pub fn socket_addr(&self) -> Result<SocketAddr> {
format!("{}:{}", self.server(), self.port()).to_socket_addrs() format!("{}:{}", self.server()?, self.port()).to_socket_addrs()
.map(|mut i| i.next().unwrap()) .map(|mut i| i.next().unwrap())
.map_err(|e| e.into()) .map_err(|e| e.into())
} }
@ -488,31 +529,27 @@ mod test {
options: Some(HashMap::new()), options: Some(HashMap::new()),
use_mock_connection: None, use_mock_connection: None,
mock_initial_value: None, mock_initial_value: None,
..Default::default()
} }
} }
#[test] #[test]
#[cfg(feature = "json")] #[cfg(feature = "json")]
fn load() { fn load_from_json() {
assert_eq!(Config::load(Path::new("client_config.json")).unwrap(), test_config()); assert_eq!(Config::load("client_config.json").unwrap(), test_config().with_path("client_config.json"));
}
#[test]
#[cfg(feature = "json")]
fn load_from_str() {
assert_eq!(Config::load("client_config.json").unwrap(), test_config());
} }
#[test] #[test]
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn load_from_toml() { fn load_from_toml() {
assert_eq!(Config::load("client_config.toml").unwrap(), test_config()); assert_eq!(Config::load("client_config.toml").unwrap(), test_config().with_path("client_config.toml"));
} }
#[test] #[test]
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
fn load_from_yaml() { fn load_from_yaml() {
assert_eq!(Config::load("client_config.yaml").unwrap(), test_config()); assert_eq!(Config::load("client_config.yaml").unwrap(), test_config().with_path("client_config.yaml"));
} }
#[test] #[test]

View file

@ -1,28 +1,28 @@
//! Utilities and shortcuts for working with IRC servers. //! Utilities and shortcuts for working with IRC servers.
//! //!
//! This module provides the [ServerExt](trait.ServerExt.html) trait which is the idiomatic way of //! 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 //! sending messages to an IRC server. This trait is automatically implemented for everything that
//! implements [Server](../trait.Server.html) and is designed to provide important functionality //! implements [`Client`](../trait.Client.html) and is designed to provide important functionality
//! without clutter. //! without clutter.
//! //!
//! # Examples //! # Examples
//! //!
//! Using these APIs, we can connect to a server and send a one-off message (in this case, //! Using these APIs, we can connect to a server and send a one-off message (in this case,
//! identifying with the server). //! identifying with the server).
//! //!
//! ```no_run //! ```no_run
//! # extern crate irc; //! # extern crate irc;
//! use irc::client::prelude::{IrcServer, ServerExt}; //! use irc::client::prelude::{IrcClient, ClientExt};
//! //!
//! # fn main() { //! # fn main() {
//! let server = IrcServer::new("config.toml").unwrap(); //! let server = IrcClient::new("config.toml").unwrap();
//! // identify and send_privmsg both come from `ServerExt` //! // identify and send_privmsg both come from `ClientExt`
//! server.identify().unwrap(); //! server.identify().unwrap();
//! server.send_privmsg("#example", "Hello, world!").unwrap(); //! server.send_privmsg("#example", "Hello, world!").unwrap();
//! # } //! # }
//! ``` //! ```
//! //!
//! `ServerExt::identify` also plays an important role in performing IRCv3 capability negotiations. //! `ClientExt::identify` also plays an important role in performing IRCv3 capability negotiations.
//! In particular, calling `identify` will close the negotiations (and otherwise indicate IRCv3 //! 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 //! compatibility). This means that all IRCv3 capability requests should be performed before calling
//! `identify`. For example: //! `identify`. For example:
@ -31,7 +31,7 @@
//! # extern crate irc; //! # extern crate irc;
//! # use irc::client::prelude::*; //! # use irc::client::prelude::*;
//! # fn main() { //! # fn main() {
//! # let server = IrcServer::new("config.toml").unwrap(); //! # let server = IrcClient::new("config.toml").unwrap();
//! server.send_cap_req(&[Capability::MultiPrefix, Capability::UserhostInNames]).unwrap(); //! server.send_cap_req(&[Capability::MultiPrefix, Capability::UserhostInNames]).unwrap();
//! server.identify().unwrap(); //! server.identify().unwrap();
//! # } //! # }
@ -46,10 +46,10 @@ use proto::{Capability, Command, Mode, NegotiationVersion};
use proto::command::CapSubCommand::{END, LS, REQ}; use proto::command::CapSubCommand::{END, LS, REQ};
use proto::command::Command::*; use proto::command::Command::*;
use proto::mode::ModeType; use proto::mode::ModeType;
use client::server::Server; use client::Client;
/// Idiomatic extensions for sending messages to an IRC server. /// Idiomatic extensions for sending messages to an IRC server as a [`Client`](../trait.Client.html).
pub trait ServerExt: Server { pub trait ClientExt: Client {
/// Sends a request for a list of server capabilities for a specific IRCv3 version. /// Sends a request for a list of server capabilities for a specific IRCv3 version.
fn send_cap_ls(&self, version: NegotiationVersion) -> Result<()> fn send_cap_ls(&self, version: NegotiationVersion) -> Result<()>
where where
@ -95,7 +95,7 @@ pub trait ServerExt: Server {
if self.config().password() != "" { if self.config().password() != "" {
self.send(PASS(self.config().password().to_owned()))?; self.send(PASS(self.config().password().to_owned()))?;
} }
self.send(NICK(self.config().nickname().to_owned()))?; self.send(NICK(self.config().nickname()?.to_owned()))?;
self.send(USER( self.send(USER(
self.config().username().to_owned(), self.config().username().to_owned(),
"0".to_owned(), "0".to_owned(),
@ -377,26 +377,22 @@ pub trait ServerExt: Server {
} }
} }
impl<S> ServerExt for S impl<C> ClientExt for C where C: Client {}
where
S: Server,
{
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::ServerExt; use super::ClientExt;
use client::data::Config; use client::data::Config;
use client::server::IrcServer; use client::IrcClient;
use client::server::test::{get_server_value, test_config}; use client::test::{get_client_value, test_config};
use proto::{ChannelMode, Mode}; use proto::{ChannelMode, Mode};
#[test] #[test]
fn identify() { fn identify() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.identify().unwrap(); client.identify().unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"CAP END\r\nNICK :test\r\n\ "CAP END\r\nNICK :test\r\n\
USER test 0 * :test\r\n" USER test 0 * :test\r\n"
); );
@ -404,14 +400,14 @@ mod test {
#[test] #[test]
fn identify_with_password() { fn identify_with_password() {
let server = IrcServer::from_config(Config { let client = IrcClient::from_config(Config {
nickname: Some(format!("test")), nickname: Some(format!("test")),
password: Some(format!("password")), password: Some(format!("password")),
..test_config() ..test_config()
}).unwrap(); }).unwrap();
server.identify().unwrap(); client.identify().unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"CAP END\r\nPASS :password\r\nNICK :test\r\n\ "CAP END\r\nPASS :password\r\nNICK :test\r\n\
USER test 0 * :test\r\n" USER test 0 * :test\r\n"
); );
@ -419,149 +415,149 @@ mod test {
#[test] #[test]
fn send_pong() { fn send_pong() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_pong("irc.test.net").unwrap(); client.send_pong("irc.test.net").unwrap();
assert_eq!(&get_server_value(server)[..], "PONG :irc.test.net\r\n"); assert_eq!(&get_client_value(client)[..], "PONG :irc.test.net\r\n");
} }
#[test] #[test]
fn send_join() { fn send_join() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_join("#test,#test2,#test3").unwrap(); client.send_join("#test,#test2,#test3").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"JOIN #test,#test2,#test3\r\n" "JOIN #test,#test2,#test3\r\n"
); );
} }
#[test] #[test]
fn send_part() { fn send_part() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_part("#test").unwrap(); client.send_part("#test").unwrap();
assert_eq!(&get_server_value(server)[..], "PART #test\r\n"); assert_eq!(&get_client_value(client)[..], "PART #test\r\n");
} }
#[test] #[test]
fn send_oper() { fn send_oper() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_oper("test", "test").unwrap(); client.send_oper("test", "test").unwrap();
assert_eq!(&get_server_value(server)[..], "OPER test :test\r\n"); assert_eq!(&get_client_value(client)[..], "OPER test :test\r\n");
} }
#[test] #[test]
fn send_privmsg() { fn send_privmsg() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_privmsg("#test", "Hi, everybody!").unwrap(); client.send_privmsg("#test", "Hi, everybody!").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"PRIVMSG #test :Hi, everybody!\r\n" "PRIVMSG #test :Hi, everybody!\r\n"
); );
} }
#[test] #[test]
fn send_notice() { fn send_notice() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_notice("#test", "Hi, everybody!").unwrap(); client.send_notice("#test", "Hi, everybody!").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"NOTICE #test :Hi, everybody!\r\n" "NOTICE #test :Hi, everybody!\r\n"
); );
} }
#[test] #[test]
fn send_topic_no_topic() { fn send_topic_no_topic() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_topic("#test", "").unwrap(); client.send_topic("#test", "").unwrap();
assert_eq!(&get_server_value(server)[..], "TOPIC #test\r\n"); assert_eq!(&get_client_value(client)[..], "TOPIC #test\r\n");
} }
#[test] #[test]
fn send_topic() { fn send_topic() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_topic("#test", "Testing stuff.").unwrap(); client.send_topic("#test", "Testing stuff.").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"TOPIC #test :Testing stuff.\r\n" "TOPIC #test :Testing stuff.\r\n"
); );
} }
#[test] #[test]
fn send_kill() { fn send_kill() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_kill("test", "Testing kills.").unwrap(); client.send_kill("test", "Testing kills.").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"KILL test :Testing kills.\r\n" "KILL test :Testing kills.\r\n"
); );
} }
#[test] #[test]
fn send_kick_no_message() { fn send_kick_no_message() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_kick("#test", "test", "").unwrap(); client.send_kick("#test", "test", "").unwrap();
assert_eq!(&get_server_value(server)[..], "KICK #test test\r\n"); assert_eq!(&get_client_value(client)[..], "KICK #test test\r\n");
} }
#[test] #[test]
fn send_kick() { fn send_kick() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_kick("#test", "test", "Testing kicks.").unwrap(); client.send_kick("#test", "test", "Testing kicks.").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"KICK #test test :Testing kicks.\r\n" "KICK #test test :Testing kicks.\r\n"
); );
} }
#[test] #[test]
fn send_mode_no_modeparams() { fn send_mode_no_modeparams() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap(); client.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap();
assert_eq!(&get_server_value(server)[..], "MODE #test +i\r\n"); assert_eq!(&get_client_value(client)[..], "MODE #test +i\r\n");
} }
#[test] #[test]
fn send_mode() { fn send_mode() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))]) client.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))])
.unwrap(); .unwrap();
assert_eq!(&get_server_value(server)[..], "MODE #test +o test\r\n"); assert_eq!(&get_client_value(client)[..], "MODE #test +o test\r\n");
} }
#[test] #[test]
fn send_samode_no_modeparams() { fn send_samode_no_modeparams() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_samode("#test", "+i", "").unwrap(); client.send_samode("#test", "+i", "").unwrap();
assert_eq!(&get_server_value(server)[..], "SAMODE #test +i\r\n"); assert_eq!(&get_client_value(client)[..], "SAMODE #test +i\r\n");
} }
#[test] #[test]
fn send_samode() { fn send_samode() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_samode("#test", "+o", "test").unwrap(); client.send_samode("#test", "+o", "test").unwrap();
assert_eq!(&get_server_value(server)[..], "SAMODE #test +o test\r\n"); assert_eq!(&get_client_value(client)[..], "SAMODE #test +o test\r\n");
} }
#[test] #[test]
fn send_sanick() { fn send_sanick() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_sanick("test", "test2").unwrap(); client.send_sanick("test", "test2").unwrap();
assert_eq!(&get_server_value(server)[..], "SANICK test test2\r\n"); assert_eq!(&get_client_value(client)[..], "SANICK test test2\r\n");
} }
#[test] #[test]
fn send_invite() { fn send_invite() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_invite("test", "#test").unwrap(); client.send_invite("test", "#test").unwrap();
assert_eq!(&get_server_value(server)[..], "INVITE test #test\r\n"); assert_eq!(&get_client_value(client)[..], "INVITE test #test\r\n");
} }
#[test] #[test]
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_ctcp() { fn send_ctcp() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_ctcp("test", "MESSAGE").unwrap(); client.send_ctcp("test", "MESSAGE").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"PRIVMSG test :\u{001}MESSAGE\u{001}\r\n" "PRIVMSG test :\u{001}MESSAGE\u{001}\r\n"
); );
} }
@ -569,10 +565,10 @@ mod test {
#[test] #[test]
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_action() { fn send_action() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_action("test", "tests.").unwrap(); client.send_action("test", "tests.").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n" "PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n"
); );
} }
@ -580,10 +576,10 @@ mod test {
#[test] #[test]
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_finger() { fn send_finger() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_finger("test").unwrap(); client.send_finger("test").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"PRIVMSG test :\u{001}FINGER\u{001}\r\n" "PRIVMSG test :\u{001}FINGER\u{001}\r\n"
); );
} }
@ -591,10 +587,10 @@ mod test {
#[test] #[test]
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_version() { fn send_version() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_version("test").unwrap(); client.send_version("test").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"PRIVMSG test :\u{001}VERSION\u{001}\r\n" "PRIVMSG test :\u{001}VERSION\u{001}\r\n"
); );
} }
@ -602,10 +598,10 @@ mod test {
#[test] #[test]
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_source() { fn send_source() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_source("test").unwrap(); client.send_source("test").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"PRIVMSG test :\u{001}SOURCE\u{001}\r\n" "PRIVMSG test :\u{001}SOURCE\u{001}\r\n"
); );
} }
@ -613,10 +609,10 @@ mod test {
#[test] #[test]
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_user_info() { fn send_user_info() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_user_info("test").unwrap(); client.send_user_info("test").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"PRIVMSG test :\u{001}USERINFO\u{001}\r\n" "PRIVMSG test :\u{001}USERINFO\u{001}\r\n"
); );
} }
@ -624,9 +620,9 @@ mod test {
#[test] #[test]
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_ctcp_ping() { fn send_ctcp_ping() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_ctcp_ping("test").unwrap(); client.send_ctcp_ping("test").unwrap();
let val = get_server_value(server); let val = get_client_value(client);
println!("{}", val); println!("{}", val);
assert!(val.starts_with("PRIVMSG test :\u{001}PING ")); assert!(val.starts_with("PRIVMSG test :\u{001}PING "));
assert!(val.ends_with("\u{001}\r\n")); assert!(val.ends_with("\u{001}\r\n"));
@ -635,10 +631,10 @@ mod test {
#[test] #[test]
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_time() { fn send_time() {
let server = IrcServer::from_config(test_config()).unwrap(); let client = IrcClient::from_config(test_config()).unwrap();
server.send_time("test").unwrap(); client.send_time("test").unwrap();
assert_eq!( assert_eq!(
&get_server_value(server)[..], &get_client_value(client)[..],
"PRIVMSG test :\u{001}TIME\u{001}\r\n" "PRIVMSG test :\u{001}TIME\u{001}\r\n"
); );
} }

File diff suppressed because it is too large Load diff

31
src/client/prelude.rs Normal file
View file

@ -0,0 +1,31 @@
//! A client-side IRC prelude, re-exporting the complete high-level IRC client API.
//!
//! # Structure
//! A connection to an IRC server is created via an `IrcClient` which is configured using a
//! `Config` struct that defines data such as which server to connect to, on what port, and
//! using what nickname. The `Client` trait provides an API for actually interacting with the
//! server once a connection has been established. This API intentionally offers only a single
//! method to send `Commands` because it makes it easy to see the whole set of possible
//! interactions with a server. The `ClientExt` trait addresses this deficiency by defining a
//! number of methods that provide a more clear and succinct interface for sending various
//! common IRC commands to the server. An `IrcReactor` can be used to create and manage multiple
//! `IrcClients` with more fine-grained control over error management.
//!
//! The various `proto` types capture details of the IRC protocol that are used throughout the
//! client API. `Message`, `Command`, and `Response` are used to send and receive messages along
//! the connection, and are naturally at the heart of communication in the IRC protocol.
//! `Capability` and `NegotiationVersion` are used to determine (with the server) what IRCv3
//! functionality to enable for the connection. Certain parts of the API offer suggestions for
//! extensions that will improve the user experience, and give examples of how to enable them
//! using `Capability`. `Mode`, `ChannelMode`, and `UserMode` are used in a high-level API for
//! 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, NegotiationVersion, Response};
pub use proto::{ChannelMode, Mode, UserMode};
pub use futures::{Future, Stream};

191
src/client/reactor.rs Normal file
View file

@ -0,0 +1,191 @@
//! 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_core::reactor::{Core, Handle};
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: Core,
handlers: Vec<Box<Future<Item = (), Error = error::IrcError>>>,
}
impl IrcReactor {
/// Creates a new reactor.
pub fn new() -> error::Result<IrcReactor> {
Ok(IrcReactor {
inner: Core::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 config = Config::default();
/// let future_client = IrcReactor::new().and_then(|mut reactor| {
/// reactor.prepare_client(&config)
/// });
/// # }
/// ```
pub fn prepare_client<'a>(&mut self, config: &'a Config) -> error::Result<IrcClientFuture<'a>> {
IrcClient::new_future(self.inner_handle(), 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 config = Config::default();
/// let client = IrcReactor::new().and_then(|mut reactor| {
/// reactor.prepare_client(&config).and_then(|future| {
/// reactor.connect_client(future)
/// })
/// });
/// # }
/// ```
pub fn connect_client(&mut self, future: IrcClientFuture) -> error::Result<IrcClient> {
self.inner.run(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 config = Config::default();
/// let client = IrcReactor::new().and_then(|mut reactor| {
/// reactor.prepare_client_and_connect(&config)
/// });
/// # }
/// ```
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, handler: F
) where F: Fn(&IrcClient, Message) -> U + 'static,
U: IntoFuture<Item = (), Error = error::IrcError> + 'static {
self.handlers.push(Box::new(client.stream().for_each(move |message| {
handler(&client, message)
})));
}
/// 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) -> 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)
/// # }
/// # 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.run(future::join_all(handlers).map(|_| ()))
}
}

View file

@ -1,11 +1,11 @@
//! The primary API for communicating with an IRC server. //! The primary API for communicating with an IRC server.
//! //!
//! This API provides the ability to connect to an IRC server via the //! This API provides the ability to connect to an IRC server via the
//! [IrcServer](struct.IrcServer.html) type. The [Server](trait.Server.html) trait that //! [`IrcServer`](struct.IrcServer.html) type. The [`Server`](trait.Server.html) trait that
//! [IrcServer](struct.IrcServer.html) implements provides methods for communicating with this //! [`IrcServer`](struct.IrcServer.html) implements provides methods for communicating with this
//! server. An extension trait, [ServerExt](./utils/trait.ServerExt.html), provides short-hand for //! server. An extension trait, [`ServerExt`](./utils/trait.ServerExt.html), provides short-hand for
//! sending a variety of important messages without referring to their entries in //! sending a variety of important messages without referring to their entries in
//! [proto::command](../../proto/command/enum.Command.html). //! [`proto::command`](../../proto/command/enum.Command.html).
//! //!
//! # Examples //! # Examples
//! //!
@ -17,7 +17,7 @@
//! use irc::client::prelude::{IrcServer, ServerExt}; //! use irc::client::prelude::{IrcServer, ServerExt};
//! //!
//! # fn main() { //! # fn main() {
//! let server = IrcServer::new("config.toml").unwrap(); //! let server = IrcServer::new("config.toml").unwrap();
//! // identify comes from `ServerExt` //! // identify comes from `ServerExt`
//! server.identify().unwrap(); //! server.identify().unwrap();
//! # } //! # }
@ -73,9 +73,9 @@ pub mod utils;
/// Trait extending all IRC streams with `for_each_incoming` convenience function. /// Trait extending all IRC streams with `for_each_incoming` convenience function.
/// ///
/// This is typically used in conjunction with [Server::stream](trait.Server.html#tymethod.stream) /// This is typically used in conjunction with [`Server::stream`](trait.Server.html#tymethod.stream)
/// in order to use an API akin to /// in order to use an API akin to
/// [Server::for_each_incoming](trait.Server.html#method.for_each_incoming). /// [`Server::for_each_incoming`](trait.Server.html#method.for_each_incoming).
/// ///
/// # Example /// # Example
/// ///
@ -85,7 +85,7 @@ pub mod utils;
/// use irc::client::prelude::EachIncomingExt; /// use irc::client::prelude::EachIncomingExt;
/// ///
/// # fn main() { /// # fn main() {
/// # let server = IrcServer::new("config.toml").unwrap(); /// # let server = IrcServer::new("config.toml").unwrap();
/// # server.identify().unwrap(); /// # server.identify().unwrap();
/// server.stream().for_each_incoming(|irc_msg| { /// server.stream().for_each_incoming(|irc_msg| {
/// match irc_msg.command { /// match irc_msg.command {
@ -97,7 +97,7 @@ pub mod utils;
/// }).unwrap(); /// }).unwrap();
/// # } /// # }
/// ``` /// ```
pub trait EachIncomingExt: Stream<Item=Message, Error=error::Error> { pub trait EachIncomingExt: Stream<Item=Message, Error=error::IrcError> {
/// Blocks on the stream, running the given function on each incoming message as they arrive. /// Blocks on the stream, running the given function on each incoming message as they arrive.
fn for_each_incoming<F>(self, mut f: F) -> error::Result<()> fn for_each_incoming<F>(self, mut f: F) -> error::Result<()>
where where
@ -111,7 +111,7 @@ pub trait EachIncomingExt: Stream<Item=Message, Error=error::Error> {
} }
} }
impl<T> EachIncomingExt for T where T: Stream<Item=Message, Error=error::Error> {} impl<T> EachIncomingExt for T where T: Stream<Item=Message, Error=error::IrcError> {}
/// An interface for communicating with an IRC server. /// An interface for communicating with an IRC server.
pub trait Server { pub trait Server {
@ -128,7 +128,7 @@ pub trait Server {
/// # extern crate irc; /// # extern crate irc;
/// # use irc::client::prelude::*; /// # use irc::client::prelude::*;
/// # fn main() { /// # fn main() {
/// # let server = IrcServer::new("config.toml").unwrap(); /// # let server = IrcServer::new("config.toml").unwrap();
/// server.send(Command::NICK("example".to_owned())).unwrap(); /// server.send(Command::NICK("example".to_owned())).unwrap();
/// server.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap(); /// server.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap();
/// # } /// # }
@ -153,7 +153,7 @@ pub trait Server {
/// # extern crate irc; /// # extern crate irc;
/// # use irc::client::prelude::{IrcServer, ServerExt, Server, Command}; /// # use irc::client::prelude::{IrcServer, ServerExt, Server, Command};
/// # fn main() { /// # fn main() {
/// # let server = IrcServer::new("config.toml").unwrap(); /// # let server = IrcServer::new("config.toml").unwrap();
/// # server.identify().unwrap(); /// # server.identify().unwrap();
/// server.for_each_incoming(|irc_msg| { /// server.for_each_incoming(|irc_msg| {
/// match irc_msg.command { /// match irc_msg.command {
@ -189,7 +189,7 @@ pub trait Server {
/// use irc::proto::caps::Capability; /// use irc::proto::caps::Capability;
/// ///
/// # fn main() { /// # fn main() {
/// # let server = IrcServer::new("config.toml").unwrap(); /// # let server = IrcServer::new("config.toml").unwrap();
/// server.send_cap_req(&[Capability::MultiPrefix]).unwrap(); /// server.send_cap_req(&[Capability::MultiPrefix]).unwrap();
/// server.identify().unwrap(); /// server.identify().unwrap();
/// # } /// # }
@ -203,6 +203,7 @@ pub trait Server {
/// traditional use cases. To learn more, you can view the documentation for the /// traditional use cases. To learn more, you can view the documentation for the
/// [futures](https://docs.rs/futures/) crate, or the tutorials for /// [futures](https://docs.rs/futures/) crate, or the tutorials for
/// [tokio](https://tokio.rs/docs/getting-started/futures/). /// [tokio](https://tokio.rs/docs/getting-started/futures/).
#[derive(Debug)]
pub struct ServerStream { pub struct ServerStream {
state: Arc<ServerState>, state: Arc<ServerState>,
stream: SplitStream<Connection>, stream: SplitStream<Connection>,
@ -210,7 +211,7 @@ pub struct ServerStream {
impl Stream for ServerStream { impl Stream for ServerStream {
type Item = Message; type Item = Message;
type Error = error::Error; type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
match try_ready!(self.stream.poll()) { match try_ready!(self.stream.poll()) {
@ -224,6 +225,7 @@ impl Stream for ServerStream {
} }
/// Thread-safe internal state for an IRC server connection. /// Thread-safe internal state for an IRC server connection.
#[derive(Debug)]
struct ServerState { struct ServerState {
/// The configuration used with this connection. /// The configuration used with this connection.
config: Config, config: Config,
@ -326,7 +328,9 @@ impl ServerState {
let alt_nicks = self.config().alternate_nicknames(); let alt_nicks = self.config().alternate_nicknames();
let index = self.alt_nick_index.read().unwrap(); let index = self.alt_nick_index.read().unwrap();
match *index { match *index {
0 => self.config().nickname(), 0 => self.config().nickname().expect(
"current_nickname should not be callable if nickname is not defined."
),
i => alt_nicks[i - 1], i => alt_nicks[i - 1],
} }
} }
@ -420,12 +424,12 @@ impl ServerState {
self.send(NICKSERV(format!( self.send(NICKSERV(format!(
"{} {} {}", "{} {} {}",
seq, seq,
self.config().nickname(), self.config().nickname()?,
self.config().nick_password() self.config().nick_password()
)))?; )))?;
} }
*index = 0; *index = 0;
self.send(NICK(self.config().nickname().to_owned()))? self.send(NICK(self.config().nickname()?.to_owned()))?
} }
self.send(NICKSERV( self.send(NICKSERV(
format!("IDENTIFY {}", self.config().nick_password()), format!("IDENTIFY {}", self.config().nick_password()),
@ -437,7 +441,16 @@ impl ServerState {
if self.config().umodes().is_empty() { if self.config().umodes().is_empty() {
Ok(()) Ok(())
} else { } else {
self.send_mode(self.current_nickname(), &Mode::as_user_modes(self.config().umodes())?) self.send_mode(
self.current_nickname(), &Mode::as_user_modes(self.config().umodes()).map_err(|e| {
error::IrcError::InvalidMessage {
string: format!(
"MODE {} {}", self.current_nickname(), self.config().umodes()
),
cause: e,
}
})?
)
} }
} }
@ -590,11 +603,13 @@ impl ServerState {
/// ///
/// The type itself provides a number of methods to create new connections, but most of the API /// The type itself provides a number of methods to create new connections, but most of the API
/// surface is in the form of the [Server](trait.Server.html) and /// surface is in the form of the [Server](trait.Server.html) and
/// [ServerExt](./utils/trait.ServerExt.html) traits that provide methods of communicating with the /// [`ServerExt`](./utils/trait.ServerExt.html) traits that provide methods of communicating with
/// server after connection. Cloning an `IrcServer` is relatively cheap, as it's equivalent to /// the server after connection. Cloning an `IrcServer` is relatively cheap, as it's equivalent to
/// cloning a single `Arc`. This may be useful for setting up multiple threads with access to one /// cloning a single `Arc`. This may be useful for setting up multiple threads with access to one
/// connection. /// connection.
#[derive(Clone)] ///
/// For a full example usage, see [`irc::client::server`](./index.html).
#[derive(Clone, Debug)]
pub struct IrcServer { pub struct IrcServer {
/// The internal, thread-safe server state. /// The internal, thread-safe server state.
state: Arc<ServerState>, state: Arc<ServerState>,
@ -667,7 +682,7 @@ impl IrcServer {
/// # extern crate irc; /// # extern crate irc;
/// # use irc::client::prelude::*; /// # use irc::client::prelude::*;
/// # fn main() { /// # fn main() {
/// let server = IrcServer::new("config.toml").unwrap(); /// let server = IrcServer::new("config.toml").unwrap();
/// # } /// # }
/// ``` /// ```
pub fn new<P: AsRef<Path>>(config: P) -> error::Result<IrcServer> { pub fn new<P: AsRef<Path>>(config: P) -> error::Result<IrcServer> {
@ -678,7 +693,8 @@ impl IrcServer {
/// immediately. Due to current design limitations, error handling here is somewhat limited. In /// immediately. Due to current design limitations, error handling here is somewhat limited. In
/// particular, failed connections will cause the program to panic because the connection /// particular, failed connections will cause the program to panic because the connection
/// attempt is made on a freshly created thread. If you need to avoid this behavior and handle /// attempt is made on a freshly created thread. If you need to avoid this behavior and handle
/// errors more gracefully, it is recommended that you use `IrcServer::new_future` instead. /// errors more gracefully, it is recommended that you use an
/// [IrcReactor](../reactor/struct.IrcReactor.html) instead.
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
@ -691,7 +707,7 @@ impl IrcServer {
/// server: Some("irc.example.com".to_owned()), /// server: Some("irc.example.com".to_owned()),
/// .. Default::default() /// .. Default::default()
/// }; /// };
/// let server = IrcServer::from_config(config).unwrap(); /// let server = IrcServer::from_config(config).unwrap();
/// # } /// # }
/// ``` /// ```
pub fn from_config(config: Config) -> error::Result<IrcServer> { pub fn from_config(config: Config) -> error::Result<IrcServer> {
@ -700,22 +716,21 @@ impl IrcServer {
let (tx_incoming, rx_incoming) = oneshot::channel(); let (tx_incoming, rx_incoming) = oneshot::channel();
let (tx_view, rx_view) = oneshot::channel(); let (tx_view, rx_view) = oneshot::channel();
let cfg = config.clone(); let mut reactor = Core::new()?;
let handle = reactor.handle();
// Attempting to connect here (as opposed to on the thread) allows more errors to happen
// immediately, rather than to occur as panics on the thread. In particular, non-resolving
// server names, and failed SSL setups will appear here.
let conn = reactor.run(Connection::new(&config, &handle)?)?;
let _ = thread::spawn(move || { let _ = thread::spawn(move || {
let mut reactor = Core::new().unwrap(); let mut reactor = Core::new().unwrap();
// Setting up internal processing stuffs.
let handle = reactor.handle();
let conn = reactor
.run(Connection::new(&cfg, &handle).unwrap())
.unwrap();
tx_view.send(conn.log_view()).unwrap(); tx_view.send(conn.log_view()).unwrap();
let (sink, stream) = conn.split(); let (sink, stream) = conn.split();
let outgoing_future = sink.send_all(rx_outgoing.map_err(|_| { let outgoing_future = sink.send_all(rx_outgoing.map_err::<error::IrcError, _>(|_| {
let res: error::Error = error::ErrorKind::ChannelError.into(); unreachable!("futures::sync::mpsc::Receiver should never return Err");
res
})).map(|_| ()).map_err(|e| panic!("{}", e)); })).map(|_| ()).map_err(|e| panic!("{}", e));
// Send the stream half back to the original thread. // Send the stream half back to the original thread.
@ -738,7 +753,9 @@ impl IrcServer {
/// 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
/// in the crate documentation for [tokio-core](http://docs.rs/tokio-core) or /// in the crate documentation for [tokio-core](http://docs.rs/tokio-core) or
/// [futures](http://docs.rs/futures). Additionally, you can find detailed tutorials on using /// [futures](http://docs.rs/futures). Additionally, you can find detailed tutorials on using
/// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). /// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). An easy
/// to use abstraction that does not require this knowledge is available via
/// [`IrcReactors`](../reactor/struct.IrcReactor.html).
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
@ -746,6 +763,7 @@ impl IrcServer {
/// # 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::server::PackedIrcServer;
/// # use irc::error; /// # use irc::error;
/// # use tokio_core::reactor::Core; /// # use tokio_core::reactor::Core;
/// # fn main() { /// # fn main() {
@ -755,14 +773,14 @@ impl IrcServer {
/// # .. Default::default() /// # .. Default::default()
/// # }; /// # };
/// let mut reactor = Core::new().unwrap(); /// let mut reactor = Core::new().unwrap();
/// let future = IrcServer::new_future(reactor.handle(), &config).unwrap(); /// let future = IrcServer::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 server = reactor.run(future).unwrap(); /// let PackedIrcServer(server, 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(server.stream().for_each(move |irc_msg| { /// reactor.run(server.stream().for_each(move |irc_msg| {
/// // processing messages works like usual /// // processing messages works like usual
/// process_msg(&server, irc_msg) /// process_msg(&server, irc_msg)
/// })).unwrap(); /// }).join(future)).unwrap();
/// # } /// # }
/// # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) } /// # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) }
/// ``` /// ```
@ -771,7 +789,7 @@ impl IrcServer {
Ok(IrcServerFuture { Ok(IrcServerFuture {
conn: Connection::new(config, &handle)?, conn: Connection::new(config, &handle)?,
handle: handle, _handle: handle,
config: config, config: config,
tx_outgoing: Some(tx_outgoing), tx_outgoing: Some(tx_outgoing),
rx_outgoing: Some(rx_outgoing), rx_outgoing: Some(rx_outgoing),
@ -797,18 +815,20 @@ impl IrcServer {
/// Interaction with this future relies on the `futures` API, but is only expected for more advanced /// 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 /// use cases. To learn more, you can view the documentation for the
/// [futures](https://docs.rs/futures/) crate, or the tutorials for /// [futures](https://docs.rs/futures/) crate, or the tutorials for
/// [tokio](https://tokio.rs/docs/getting-started/futures/). /// [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 IrcServerFuture<'a> { pub struct IrcServerFuture<'a> {
conn: ConnectionFuture<'a>, conn: ConnectionFuture<'a>,
handle: Handle, _handle: Handle,
config: &'a Config, config: &'a Config,
tx_outgoing: Option<UnboundedSender<Message>>, tx_outgoing: Option<UnboundedSender<Message>>,
rx_outgoing: Option<UnboundedReceiver<Message>>, rx_outgoing: Option<UnboundedReceiver<Message>>,
} }
impl<'a> Future for IrcServerFuture<'a> { impl<'a> Future for IrcServerFuture<'a> {
type Item = IrcServer; type Item = PackedIrcServer;
type Error = error::Error; type Error = error::IrcError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let conn = try_ready!(self.conn.poll()); let conn = try_ready!(self.conn.poll());
@ -816,22 +836,29 @@ impl<'a> Future for IrcServerFuture<'a> {
let view = conn.log_view(); let view = conn.log_view();
let (sink, stream) = conn.split(); let (sink, stream) = conn.split();
let outgoing_future = sink.send_all(self.rx_outgoing.take().unwrap().map_err(|()| { let outgoing_future = sink.send_all(
let res: error::Error = error::ErrorKind::ChannelError.into(); self.rx_outgoing.take().unwrap().map_err::<error::IrcError, _>(|()| {
res unreachable!("futures::sync::mpsc::Receiver should never return Err");
})).map(|_| ()).map_err(|e| panic!(e)); })
).map(|_| ());
self.handle.spawn(outgoing_future); let server = IrcServer {
Ok(Async::Ready(IrcServer {
state: Arc::new(ServerState::new( state: Arc::new(ServerState::new(
stream, self.tx_outgoing.take().unwrap(), self.config.clone() stream, self.tx_outgoing.take().unwrap(), self.config.clone()
)), )),
view: view, view: view,
})) };
Ok(Async::Ready(PackedIrcServer(server, Box::new(outgoing_future))))
} }
} }
/// An `IrcServer` packaged with a future that drives its message sending. In order for the server
/// 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 PackedIrcServer(pub IrcServer, pub Box<Future<Item = (), Error = error::IrcError>>);
#[cfg(test)] #[cfg(test)]
mod test { mod test {

View file

@ -44,11 +44,11 @@ where
inner: inner, inner: inner,
burst_timer: tokio_timer::wheel().build(), burst_timer: tokio_timer::wheel().build(),
rolling_burst_window: VecDeque::new(), rolling_burst_window: VecDeque::new(),
burst_window_length: config.burst_window_length() as u64, burst_window_length: u64::from(config.burst_window_length()),
max_burst_messages: config.max_messages_in_burst() as u64, max_burst_messages: u64::from(config.max_messages_in_burst()),
current_burst_messages: 0, current_burst_messages: 0,
ping_timer: timer.interval(Duration::from_secs(config.ping_time() as u64)), ping_timer: timer.interval(Duration::from_secs(u64::from(config.ping_time()))),
ping_timeout: config.ping_timeout() as u64, ping_timeout: u64::from(config.ping_timeout()),
last_ping_data: String::new(), last_ping_data: String::new(),
last_ping_sent: Instant::now(), last_ping_sent: Instant::now(),
last_pong_received: Instant::now(), last_pong_received: Instant::now(),
@ -88,12 +88,12 @@ where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite,
{ {
type Item = Message; type Item = Message;
type Error = error::Error; type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
if self.ping_timed_out() { if self.ping_timed_out() {
self.close()?; self.close()?;
return Err(error::ErrorKind::PingTimeout.into()) return Err(error::IrcError::PingTimeout)
} }
let timer_poll = self.ping_timer.poll()?; let timer_poll = self.ping_timer.poll()?;
@ -144,12 +144,12 @@ where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite,
{ {
type SinkItem = Message; type SinkItem = Message;
type SinkError = error::Error; type SinkError = error::IrcError;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> { fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
if self.ping_timed_out() { if self.ping_timed_out() {
self.close()?; self.close()?;
Err(error::ErrorKind::PingTimeout.into()) Err(error::IrcError::PingTimeout)
} else { } else {
// Check if the oldest message in the rolling window is discounted. // Check if the oldest message in the rolling window is discounted.
if let Async::Ready(()) = self.rolling_burst_window_front()? { if let Async::Ready(()) = self.rolling_burst_window_front()? {
@ -180,7 +180,7 @@ where
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
if self.ping_timed_out() { if self.ping_timed_out() {
self.close()?; self.close()?;
Err(error::ErrorKind::PingTimeout.into()) Err(error::IrcError::PingTimeout)
} else { } else {
Ok(self.inner.poll_complete()?) Ok(self.inner.poll_complete()?)
} }
@ -201,16 +201,12 @@ pub struct LogView {
impl LogView { impl LogView {
/// Gets a read guard for all the messages sent on the transport. /// Gets a read guard for all the messages sent on the transport.
pub fn sent(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> { pub fn sent(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> {
self.sent.read().map_err( self.sent.read().map_err(|_| error::IrcError::PoisonedLog)
|_| error::ErrorKind::PoisonedLog.into(),
)
} }
/// Gets a read guard for all the messages received on the transport. /// Gets a read guard for all the messages received on the transport.
pub fn received(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> { pub fn received(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> {
self.received.read().map_err( self.received.read().map_err(|_| error::IrcError::PoisonedLog)
|_| error::ErrorKind::PoisonedLog.into(),
)
} }
} }
@ -250,13 +246,13 @@ where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite,
{ {
type Item = Message; type Item = Message;
type Error = error::Error; type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
match try_ready!(self.inner.poll()) { match try_ready!(self.inner.poll()) {
Some(msg) => { Some(msg) => {
let recv: error::Result<_> = self.view.received.write().map_err(|_| { let recv: error::Result<_> = self.view.received.write().map_err(|_| {
error::ErrorKind::PoisonedLog.into() error::IrcError::PoisonedLog
}); });
recv?.push(msg.clone()); recv?.push(msg.clone());
Ok(Async::Ready(Some(msg))) Ok(Async::Ready(Some(msg)))
@ -271,12 +267,12 @@ where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite,
{ {
type SinkItem = Message; type SinkItem = Message;
type SinkError = error::Error; type SinkError = error::IrcError;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> { fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
let res = self.inner.start_send(item.clone())?; let res = self.inner.start_send(item.clone())?;
let sent: error::Result<_> = self.view.sent.write().map_err(|_| { let sent: error::Result<_> = self.view.sent.write().map_err(|_| {
error::ErrorKind::PoisonedLog.into() error::IrcError::PoisonedLog
}); });
sent?.push(item); sent?.push(item);
Ok(res) Ok(res)

View file

@ -1,58 +1,233 @@
//! Errors for `irc` crate using `error_chain`. //! Errors for `irc` crate using `failure`.
#![allow(missing_docs)] use std::io::Error as IoError;
use std::sync::mpsc::RecvError;
error_chain! { use futures::sync::mpsc::SendError;
foreign_links { use futures::sync::oneshot::Canceled;
Io(::std::io::Error); use native_tls::Error as TlsError;
Tls(::native_tls::Error); #[cfg(feature = "json")]
Recv(::std::sync::mpsc::RecvError); use serde_json::Error as JsonError;
SendMessage(::futures::sync::mpsc::SendError<::proto::Message>); #[cfg(feature = "yaml")]
OneShotCancelled(::futures::sync::oneshot::Canceled); use serde_yaml::Error as YamlError;
Timer(::tokio_timer::TimerError); use tokio_timer::TimerError;
} #[cfg(feature = "toml")]
use toml::de::Error as TomlReadError;
#[cfg(feature = "toml")]
use toml::ser::Error as TomlWriteError;
errors { use proto::Message;
/// A parsing error for empty strings as messages.
ParseEmpty {
description("Cannot parse an empty string as a message.")
display("Cannot parse an empty string as a message.")
}
/// A parsing error for invalid or missing commands in messages. /// A specialized `Result` type for the `irc` crate.
InvalidCommand { pub type Result<T> = ::std::result::Result<T, IrcError>;
description("Message contained a missing or invalid Command.")
display("Message contained a missing or invalid Command.")
}
/// A parsing error for failures in subcommand parsing (e.g. CAP and metadata). /// The main crate-wide error type.
SubCommandParsingFailed { #[derive(Debug, Fail)]
description("Failed to parse an IRC subcommand.") pub enum IrcError {
display("Failed to parse an IRC subcommand.") /// An internal I/O error.
} #[fail(display = "an io error occurred")]
Io(#[cause] IoError),
/// Failed to parse a mode correctly. /// An internal TLS error.
ModeParsingFailed { #[fail(display = "a TLS error occurred")]
description("Failed to parse a mode correctly.") Tls(#[cause] TlsError),
display("Failed to parse a mode correctly.")
}
/// An error occurred on one of the internal channels of the `IrcServer`. /// An internal synchronous channel closed.
ChannelError { #[fail(display = "a sync channel closed")]
description("An error occured on one of the IrcServer's internal channels.") SyncChannelClosed(#[cause] RecvError),
display("An error occured on one of the IrcServer's internal channels.")
}
/// An error occured causing a mutex for a logged transport to be poisoned. /// An internal asynchronous channel closed.
PoisonedLog { #[fail(display = "an async channel closed")]
description("An error occured causing a mutex for a logged transport to be poisoned.") AsyncChannelClosed(#[cause] SendError<Message>),
display("An error occured causing a mutex for a logged transport to be poisoned.")
}
/// Connection timed out due to no ping response. /// An internal oneshot channel closed.
PingTimeout { #[fail(display = "a oneshot channel closed")]
description("The connection timed out due to no ping response.") OneShotCanceled(#[cause] Canceled),
display("The connection timed out due to no ping response.")
} /// An internal timer error.
#[fail(display = "timer failed")]
Timer(#[cause] TimerError),
/// Error for invalid configurations.
#[fail(display = "invalid config: {}", path)]
InvalidConfig {
/// The path to the configuration, or "<none>" if none specified.
path: String,
/// The detailed configuration error.
#[cause]
cause: ConfigError,
},
/// Error for invalid messages.
#[fail(display = "invalid message: {}", string)]
InvalidMessage {
/// The string that failed to parse.
string: String,
/// The detailed message parsing error.
#[cause]
cause: MessageParseError,
},
/// Mutex for a logged transport was poisoned making the log inaccessible.
#[fail(display = "mutex for a logged transport was poisoned")]
PoisonedLog,
/// Ping timed out due to no response.
#[fail(display = "connection reset: no ping response")]
PingTimeout,
/// Failed to lookup an unknown codec.
#[fail(display = "unknown codec: {}", codec)]
UnknownCodec {
/// The attempted codec.
codec: String,
},
/// Failed to encode or decode something with the given codec.
#[fail(display = "codec {} failed: {}", codec, data)]
CodecFailed {
/// The canonical codec name.
codec: &'static str,
/// The data that failed to encode or decode.
data: String,
},
}
/// Errors that occur when parsing messages.
#[derive(Debug, Fail)]
pub enum MessageParseError {
/// The message was empty.
#[fail(display = "empty message")]
EmptyMessage,
/// The command was invalid (i.e. missing).
#[fail(display = "invalid command")]
InvalidCommand,
/// The mode string was malformed.
#[fail(display = "invalid mode string: {}", string)]
InvalidModeString {
/// The invalid mode string.
string: String,
/// The detailed mode parsing error.
#[cause]
cause: ModeParseError,
},
/// The subcommand used was invalid.
#[fail(display = "invalid {} subcommand: {}", cmd, sub)]
InvalidSubcommand {
/// The command whose invalid subcommand was referenced.
cmd: &'static str,
/// The invalid subcommand.
sub: String,
}
}
/// Errors that occur while parsing mode strings.
#[derive(Debug, Fail)]
pub enum ModeParseError {
/// Invalid modifier used in a mode string (only + and - are valid).
#[fail(display = "invalid mode modifier: {}", modifier)]
InvalidModeModifier {
/// The invalid mode modifier.
modifier: char,
},
/// Missing modifier used in a mode string.
#[fail(display = "missing mode modifier")]
MissingModeModifier,
}
/// Errors that occur with configurations.
#[derive(Debug, Fail)]
pub enum ConfigError {
/// Failed to parse as TOML.
#[cfg(feature = "toml")]
#[fail(display = "invalid toml")]
InvalidToml(#[cause] TomlError),
/// Failed to parse as JSON.
#[cfg(feature = "json")]
#[fail(display = "invalid json")]
InvalidJson(#[cause] JsonError),
/// Failed to parse as YAML.
#[cfg(feature = "yaml")]
#[fail(display = "invalid yaml")]
InvalidYaml(#[cause] YamlError),
/// Failed to parse the given format because it was disabled at compile-time.
#[fail(display = "config format disabled: {}", format)]
ConfigFormatDisabled {
/// The disabled file format.
format: &'static str,
},
/// Could not identify the given file format.
#[fail(display = "config format unknown: {}", format)]
UnknownConfigFormat {
/// The unknown file extension.
format: String,
},
/// File was missing an extension to identify file format.
#[fail(display = "missing format extension")]
MissingExtension,
/// Configuration does not specify a nickname.
#[fail(display = "nickname not specified")]
NicknameNotSpecified,
/// Configuration does not specify a server.
#[fail(display = "server not specified")]
ServerNotSpecified,
}
/// A wrapper that combines toml's serialization and deserialization errors.
#[cfg(feature = "toml")]
#[derive(Debug, Fail)]
pub enum TomlError {
/// A TOML deserialization error.
#[fail(display = "deserialization failed")]
Read(#[cause] TomlReadError),
/// A TOML serialization error.
#[fail(display = "serialization failed")]
Write(#[cause] TomlWriteError),
}
impl From<IoError> for IrcError {
fn from(e: IoError) -> IrcError {
IrcError::Io(e)
}
}
impl From<TlsError> for IrcError {
fn from(e: TlsError) -> IrcError {
IrcError::Tls(e)
}
}
impl From<RecvError> for IrcError {
fn from(e: RecvError) -> IrcError {
IrcError::SyncChannelClosed(e)
}
}
impl From<SendError<Message>> for IrcError {
fn from(e: SendError<Message>) -> IrcError {
IrcError::AsyncChannelClosed(e)
}
}
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)
} }
} }

View file

@ -1,18 +1,18 @@
//! A simple, thread-safe, and async-friendly library for IRC clients. //! A simple, thread-safe, and async-friendly library for IRC clients.
//! //!
//! # Quick Start //! # Quick Start
//! The main public API is entirely exported in [client::prelude](./client/prelude/index.html). This //! The main public API is entirely exported in [`client::prelude`](./client/prelude/index.html).
//! should include everything necessary to write an IRC client or bot. //! This should include everything necessary to write an IRC client or bot.
//! //!
//! # A Whirlwind Tour //! # A Whirlwind Tour
//! The irc crate is divided into two main modules: [client](./client/index.html) and //! The irc crate is divided into two main modules: [`client`](./client/index.html) and
//! [proto](./proto/index.html). As the names suggest, the client module captures the whole of the //! [`proto`](./proto/index.html). As the names suggest, the `client` module captures the whole of
//! client-side functionality, while the proto module features general components of an IRC protocol //! the client-side functionality, while the `proto` module features general components of an IRC
//! implementation that could in principle be used in either client or server software. Both modules //! protocol implementation that could in principle be used in either client or server software.
//! feature a number of components that are low-level and can be used to build alternative APIs for //! Both modules feature a number of components that are low-level and can be used to build
//! the IRC protocol. For the average user, the higher-level components for an IRC client are all //! alternative APIs for the IRC protocol. For the average user, the higher-level components for an
//! re-exported in [client::prelude](./client/prelude/index.html). That module serves as the best //! IRC client are all re-exported in [`client::prelude`](./client/prelude/index.html). That module
//! starting point for a new user trying to understand the high-level API. //! serves as the best starting point for a new user trying to understand the high-level API.
//! //!
//! # Example //! # Example
//! //!
@ -22,31 +22,29 @@
//! //!
//! # fn main() { //! # fn main() {
//! // configuration is loaded from config.toml into a Config //! // configuration is loaded from config.toml into a Config
//! let server = IrcServer::new("config.toml").unwrap(); //! let client = IrcClient::new("config.toml").unwrap();
//! // identify comes from ServerExt //! // identify comes from ClientExt
//! server.identify().unwrap(); //! client.identify().unwrap();
//! // for_each_incoming comes from Server //! // for_each_incoming comes from Client
//! server.for_each_incoming(|irc_msg| { //! client.for_each_incoming(|irc_msg| {
//! // irc_msg is a Message //! // irc_msg is a Message
//! match irc_msg.command { //! if let Command::PRIVMSG(channel, message) = irc_msg.command {
//! Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) { //! if message.contains(client.current_nickname()) {
//! // send_privmsg comes from ServerExt //! // send_privmsg comes from ClientExt
//! server.send_privmsg(&channel, "beep boop").unwrap(); //! client.send_privmsg(&channel, "beep boop").unwrap();
//! }
//! } //! }
//! _ => ()
//! }
//! }).unwrap(); //! }).unwrap();
//! # } //! # }
//! ``` //! ```
#![warn(missing_docs)] #![warn(missing_docs)]
#![recursion_limit="128"]
extern crate bufstream; extern crate bufstream;
extern crate bytes; extern crate bytes;
extern crate chrono; extern crate chrono;
#[macro_use] #[macro_use]
extern crate error_chain; extern crate failure;
extern crate encoding; extern crate encoding;
#[macro_use] #[macro_use]
extern crate futures; extern crate futures;
@ -72,7 +70,7 @@ pub mod client;
pub mod error; pub mod error;
pub mod proto; pub mod proto;
const VERSION_STR: &'static str = concat!( const VERSION_STR: &str = concat!(
env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"),
":", ":",
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),

View file

@ -2,7 +2,7 @@
use std::ascii::AsciiExt; use std::ascii::AsciiExt;
use std::str::FromStr; use std::str::FromStr;
use error; use error::MessageParseError;
use proto::{ChannelExt, ChannelMode, Mode, Response, UserMode}; use proto::{ChannelExt, ChannelMode, Mode, Response, UserMode};
/// List of all client commands as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812). This /// List of all client commands as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812). This
@ -446,7 +446,7 @@ impl<'a> From<&'a Command> for String {
impl Command { impl Command {
/// Constructs a new Command. /// Constructs a new Command.
pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> error::Result<Command> { pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> Result<Command, MessageParseError> {
Ok(if cmd.eq_ignore_ascii_case("PASS") { Ok(if cmd.eq_ignore_ascii_case("PASS") {
match suffix { match suffix {
Some(suffix) => { Some(suffix) => {
@ -1653,8 +1653,9 @@ impl CapSubCommand {
} }
impl FromStr for CapSubCommand { impl FromStr for CapSubCommand {
type Err = error::Error; type Err = MessageParseError;
fn from_str(s: &str) -> error::Result<CapSubCommand> {
fn from_str(s: &str) -> Result<CapSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("LS") { if s.eq_ignore_ascii_case("LS") {
Ok(CapSubCommand::LS) Ok(CapSubCommand::LS)
} else if s.eq_ignore_ascii_case("LIST") { } else if s.eq_ignore_ascii_case("LIST") {
@ -1672,7 +1673,10 @@ impl FromStr for CapSubCommand {
} else if s.eq_ignore_ascii_case("DEL") { } else if s.eq_ignore_ascii_case("DEL") {
Ok(CapSubCommand::DEL) Ok(CapSubCommand::DEL)
} else { } else {
Err(error::ErrorKind::SubCommandParsingFailed.into()) Err(MessageParseError::InvalidSubcommand {
cmd: "CAP",
sub: s.to_owned(),
})
} }
} }
} }
@ -1704,8 +1708,9 @@ impl MetadataSubCommand {
} }
impl FromStr for MetadataSubCommand { impl FromStr for MetadataSubCommand {
type Err = error::Error; type Err = MessageParseError;
fn from_str(s: &str) -> error::Result<MetadataSubCommand> {
fn from_str(s: &str) -> Result<MetadataSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("GET") { if s.eq_ignore_ascii_case("GET") {
Ok(MetadataSubCommand::GET) Ok(MetadataSubCommand::GET)
} else if s.eq_ignore_ascii_case("LIST") { } else if s.eq_ignore_ascii_case("LIST") {
@ -1715,7 +1720,10 @@ impl FromStr for MetadataSubCommand {
} else if s.eq_ignore_ascii_case("CLEAR") { } else if s.eq_ignore_ascii_case("CLEAR") {
Ok(MetadataSubCommand::CLEAR) Ok(MetadataSubCommand::CLEAR)
} else { } else {
Err(error::ErrorKind::SubCommandParsingFailed.into()) Err(MessageParseError::InvalidSubcommand {
cmd: "METADATA",
sub: s.to_owned(),
})
} }
} }
} }
@ -1743,8 +1751,9 @@ impl BatchSubCommand {
} }
impl FromStr for BatchSubCommand { impl FromStr for BatchSubCommand {
type Err = error::Error; type Err = MessageParseError;
fn from_str(s: &str) -> error::Result<BatchSubCommand> {
fn from_str(s: &str) -> Result<BatchSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("NETSPLIT") { if s.eq_ignore_ascii_case("NETSPLIT") {
Ok(BatchSubCommand::NETSPLIT) Ok(BatchSubCommand::NETSPLIT)
} else if s.eq_ignore_ascii_case("NETJOIN") { } else if s.eq_ignore_ascii_case("NETJOIN") {

View file

@ -20,7 +20,7 @@ impl IrcCodec {
impl Decoder for IrcCodec { impl Decoder for IrcCodec {
type Item = Message; type Item = Message;
type Error = error::Error; type Error = error::IrcError;
fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<Message>> { fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<Message>> {
self.inner.decode(src).and_then(|res| { self.inner.decode(src).and_then(|res| {
@ -31,7 +31,7 @@ impl Decoder for IrcCodec {
impl Encoder for IrcCodec { impl Encoder for IrcCodec {
type Item = Message; type Item = Message;
type Error = error::Error; type Error = error::IrcError;
fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> { fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> {

View file

@ -28,7 +28,7 @@ impl LineCodec {
impl Decoder for LineCodec { impl Decoder for LineCodec {
type Item = String; type Item = String;
type Error = error::Error; type Error = error::IrcError;
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(n) = src.as_ref().iter().position(|b| *b == b'\n') {
@ -53,7 +53,7 @@ impl Decoder for LineCodec {
impl Encoder for LineCodec { impl Encoder for LineCodec {
type Item = String; type Item = String;
type Error = error::Error; type Error = error::IrcError;
fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> { fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> {
// Encode the message using the codec's encoding. // Encode the message using the codec's encoding.

View file

@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
use std::str::FromStr; use std::str::FromStr;
use error; use error;
use error::{Error, ErrorKind}; use error::{IrcError, MessageParseError};
use proto::{Command, ChannelExt}; use proto::{Command, ChannelExt};
/// A data structure representing an IRC message according to the protocol specification. It /// A data structure representing an IRC message according to the protocol specification. It
@ -43,7 +43,7 @@ impl Message {
command: &str, command: &str,
args: Vec<&str>, args: Vec<&str>,
suffix: Option<&str>, suffix: Option<&str>,
) -> error::Result<Message> { ) -> Result<Message, MessageParseError> {
Message::with_tags(None, prefix, command, args, suffix) Message::with_tags(None, prefix, command, args, suffix)
} }
@ -56,7 +56,7 @@ impl Message {
command: &str, command: &str,
args: Vec<&str>, args: Vec<&str>,
suffix: Option<&str>, suffix: Option<&str>,
) -> error::Result<Message> { ) -> Result<Message, error::MessageParseError> {
Ok(Message { Ok(Message {
tags: tags, tags: tags,
prefix: prefix.map(|s| s.to_owned()), prefix: prefix.map(|s| s.to_owned()),
@ -85,7 +85,7 @@ impl Message {
s.find('@'), s.find('@'),
s.find('.'), s.find('.'),
) { ) {
(Some(i), _, _) => Some(&s[..i]), // <nick> '!' <user> [ '@' <host> ] (Some(i), _, _) | // <nick> '!' <user> [ '@' <host> ]
(None, Some(i), _) => Some(&s[..i]), // <nick> '@' <host> (None, Some(i), _) => Some(&s[..i]), // <nick> '@' <host>
(None, None, None) => Some(s), // <nick> (None, None, None) => Some(s), // <nick>
_ => None, // <servername> _ => None, // <servername>
@ -170,13 +170,18 @@ impl From<Command> for Message {
} }
impl FromStr for Message { impl FromStr for Message {
type Err = Error; type Err = IrcError;
fn from_str(s: &str) -> Result<Message, Self::Err> { fn from_str(s: &str) -> Result<Message, Self::Err> {
let mut state = s;
if s.is_empty() { if s.is_empty() {
return Err(ErrorKind::ParseEmpty.into()); return Err(IrcError::InvalidMessage {
string: s.to_owned(),
cause: MessageParseError::EmptyMessage,
})
} }
let mut state = s;
let tags = if state.starts_with('@') { let tags = if state.starts_with('@') {
let tags = state.find(' ').map(|i| &state[1..i]); let tags = state.find(' ').map(|i| &state[1..i]);
state = state.find(' ').map_or("", |i| &state[i + 1..]); state = state.find(' ').map_or("", |i| &state[i + 1..]);
@ -193,6 +198,7 @@ impl FromStr for Message {
} else { } else {
None None
}; };
let prefix = if state.starts_with(':') { let prefix = if state.starts_with(':') {
let prefix = state.find(' ').map(|i| &state[1..i]); let prefix = state.find(' ').map(|i| &state[1..i]);
state = state.find(' ').map_or("", |i| &state[i + 1..]); state = state.find(' ').map_or("", |i| &state[i + 1..]);
@ -200,6 +206,7 @@ impl FromStr for Message {
} else { } else {
None None
}; };
let line_ending_len = if state.ends_with("\r\n") { let line_ending_len = if state.ends_with("\r\n") {
"\r\n" "\r\n"
} else if state.ends_with('\r') { } else if state.ends_with('\r') {
@ -209,6 +216,7 @@ impl FromStr for Message {
} else { } else {
"" ""
}.len(); }.len();
let suffix = if state.contains(" :") { let suffix = if state.contains(" :") {
let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - line_ending_len]); let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - line_ending_len]);
state = state.find(" :").map_or("", |i| &state[..i + 1]); state = state.find(" :").map_or("", |i| &state[..i + 1]);
@ -217,24 +225,33 @@ impl FromStr for Message {
state = &state[..state.len() - line_ending_len]; state = &state[..state.len() - line_ending_len];
None None
}; };
let command = match state.find(' ').map(|i| &state[..i]) { let command = match state.find(' ').map(|i| &state[..i]) {
Some(cmd) => { Some(cmd) => {
state = state.find(' ').map_or("", |i| &state[i + 1..]); state = state.find(' ').map_or("", |i| &state[i + 1..]);
cmd cmd
} }
// If there's no arguments but the "command" starts with colon, it's not a command. // If there's no arguments but the "command" starts with colon, it's not a command.
None if state.starts_with(":") => return Err(ErrorKind::InvalidCommand.into()), None if state.starts_with(':') => return Err(IrcError::InvalidMessage {
string: s.to_owned(),
cause: MessageParseError::InvalidCommand,
}),
// If there's no arguments following the command, the rest of the state is the command. // If there's no arguments following the command, the rest of the state is the command.
None => { None => {
let cmd = state; let cmd = state;
state = ""; state = "";
cmd cmd
}, },
}; };
let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect(); let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect();
Message::with_tags(tags, prefix, command, args, suffix)
.map_err(|_| ErrorKind::InvalidCommand.into()) Message::with_tags(tags, prefix, command, args, suffix).map_err(|e| {
IrcError::InvalidMessage {
string: s.to_owned(),
cause: e,
}
})
} }
} }

View file

@ -1,7 +1,9 @@
//! A module defining an API for IRC user and channel modes. //! A module defining an API for IRC user and channel modes.
use std::fmt; use std::fmt;
use error; use error::MessageParseError;
use error::MessageParseError::InvalidModeString;
use error::ModeParseError::*;
use proto::Command; use proto::Command;
/// A marker trait for different kinds of Modes. /// A marker trait for different kinds of Modes.
@ -48,10 +50,10 @@ impl ModeType for UserMode {
} }
impl UserMode { impl UserMode {
fn from_char(c: char) -> error::Result<UserMode> { fn from_char(c: char) -> UserMode {
use self::UserMode::*; use self::UserMode::*;
Ok(match c { match c {
'a' => Away, 'a' => Away,
'i' => Invisible, 'i' => Invisible,
'w' => Wallops, 'w' => Wallops,
@ -61,7 +63,7 @@ impl UserMode {
's' => ServerNotices, 's' => ServerNotices,
'x' => MaskedHost, 'x' => MaskedHost,
_ => Unknown(c), _ => Unknown(c),
}) }
} }
} }
@ -141,10 +143,10 @@ impl ModeType for ChannelMode {
} }
impl ChannelMode { impl ChannelMode {
fn from_char(c: char) -> error::Result<ChannelMode> { fn from_char(c: char) -> ChannelMode {
use self::ChannelMode::*; use self::ChannelMode::*;
Ok(match c { match c {
'b' => Ban, 'b' => Ban,
'e' => Exception, 'e' => Exception,
'l' => Limit, 'l' => Limit,
@ -162,7 +164,7 @@ impl ChannelMode {
'h' => Halfop, 'h' => Halfop,
'v' => Voice, 'v' => Voice,
_ => Unknown(c), _ => Unknown(c),
}) }
} }
} }
@ -242,7 +244,7 @@ enum PlusMinus {
impl Mode<UserMode> { impl Mode<UserMode> {
// TODO: turning more edge cases into errors. // TODO: turning more edge cases into errors.
/// Parses the specified mode string as user modes. /// Parses the specified mode string as user modes.
pub fn as_user_modes(s: &str) -> error::Result<Vec<Mode<UserMode>>> { pub fn as_user_modes(s: &str) -> Result<Vec<Mode<UserMode>>, MessageParseError> {
use self::PlusMinus::*; use self::PlusMinus::*;
let mut res = vec![]; let mut res = vec![];
@ -255,11 +257,18 @@ impl Mode<UserMode> {
let init = match chars.next() { let init = match chars.next() {
Some('+') => Plus, Some('+') => Plus,
Some('-') => Minus, Some('-') => Minus,
_ => return Err(error::ErrorKind::ModeParsingFailed.into()), 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 { for c in chars {
let mode = UserMode::from_char(c)?; let mode = UserMode::from_char(c);
let arg = if mode.takes_arg() { let arg = if mode.takes_arg() {
pieces.next() pieces.next()
} else { } else {
@ -281,7 +290,7 @@ impl Mode<UserMode> {
impl Mode<ChannelMode> { impl Mode<ChannelMode> {
// TODO: turning more edge cases into errors. // TODO: turning more edge cases into errors.
/// Parses the specified mode string as channel modes. /// Parses the specified mode string as channel modes.
pub fn as_channel_modes(s: &str) -> error::Result<Vec<Mode<ChannelMode>>> { pub fn as_channel_modes(s: &str) -> Result<Vec<Mode<ChannelMode>>, MessageParseError> {
use self::PlusMinus::*; use self::PlusMinus::*;
let mut res = vec![]; let mut res = vec![];
@ -294,11 +303,18 @@ impl Mode<ChannelMode> {
let init = match chars.next() { let init = match chars.next() {
Some('+') => Plus, Some('+') => Plus,
Some('-') => Minus, Some('-') => Minus,
_ => return Err(error::ErrorKind::ModeParsingFailed.into()), 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 { for c in chars {
let mode = ChannelMode::from_char(c)?; let mode = ChannelMode::from_char(c);
let arg = if mode.takes_arg() { let arg = if mode.takes_arg() {
pieces.next() pieces.next()
} else { } else {