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
@ -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
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
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
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
complaints will be reviewed and investigated and will result in a response that
reported by contacting the lead project maintainer at [awe@pdgn.co](mailto:awe@pdgn.co).
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
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

View file

@ -1,6 +1,6 @@
[package]
name = "irc"
version = "0.12.8"
version = "0.13.0"
description = "A simple, thread-safe, and async-friendly library for IRC clients."
authors = ["Aaron Weiss <awe@pdgn.co>"]
license = "MPL-2.0"
@ -14,7 +14,7 @@ readme = "README.md"
travis-ci = { repository = "aatxe/irc" }
[features]
default = ["ctcp", "json", "toml"]
default = ["ctcp", "toml"]
ctcp = []
nochanlists = []
json = ["serde_json"]
@ -25,7 +25,7 @@ bufstream = "0.1"
bytes = "0.4"
chrono = "0.4"
encoding = "0.2"
error-chain = "0.10"
failure = "0.1"
futures = "0.1"
log = "0.3"
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]
[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
[cr-badge]: https://img.shields.io/crates/v/irc.svg
[cr]: https://crates.io/crates/irc
@ -19,7 +19,7 @@ can be disabled accordingly.
## 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
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
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 files with `serde` to write configurations. By default, we support JSON and TOML. As of
0.12.4, TOML is the preferred configuration format. We have bundled a conversion tool as
`convertconf` in the examples. In a future version, we will likely disable JSON by default.
Additionally, you can enable the optional `yaml` feature to get support for YAML as well. All the
configuration fields are optional, and thus any of them can be omitted (though, omitting a
nickname or server will cause the program to fail for obvious reasons).
loading files with `serde` to write configurations. The default configuration format is TOML,
though there is optional support for JSON and YAML via the optional `json` and `yaml` features. All
the configuration fields are optional, and can thus be omitted, but a working configuration requires
at least a `server` and `nickname`. You can find detailed explanations of the configuration format
[here](https://docs.rs/irc/0.12.8/irc/client/data/config/struct.Config.html#fields).
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
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
important, and to that end, we've adopted the
[Contributor Convenant](https://www.contributor-covenant.org).
important, and to that end, we've adopted an explicit Code of Conduct found in `CODE_OF_CONDUCT.md`.

View file

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

View file

@ -1,8 +1,6 @@
extern crate futures;
extern crate irc;
use std::default::Default;
use futures::stream::MergedItem;
use irc::error;
use irc::client::prelude::*;
@ -13,35 +11,38 @@ fn main() {
channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default()
};
let cfg2 = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
nickname: Some("bananas".to_owned()),
server: Some("irc.fyrechat.net".to_owned()),
channels: Some(vec!["#irc-crate".to_owned()]),
use_ssl: Some(true),
..Default::default()
};
let server1 = IrcServer::from_config(cfg1).unwrap();
let server2 = IrcServer::from_config(cfg2).unwrap();
server1.identify().unwrap();
server2.identify().unwrap();
let configs = vec![cfg1, cfg2];
server1.stream().merge(server2.stream()).for_each(|pair| match pair {
MergedItem::First(message) => process_msg(&server1, message),
MergedItem::Second(message) => process_msg(&server2, message),
MergedItem::Both(msg1, msg2) => {
process_msg(&server1, msg1).unwrap();
process_msg(&server2, msg2)
}
}).wait().unwrap()
let mut reactor = IrcReactor::new().unwrap();
for config in configs {
// Immediate errors like failure to resolve the server's domain or to establish any connection will
// manifest here in the result of prepare_client_and_connect.
let client = reactor.prepare_client_and_connect(&config).unwrap();
client.identify().unwrap();
// Here, we tell the reactor to setup this client for future handling (in run) using the specified
// handler function process_msg.
reactor.register_client_with_handler(client, process_msg);
}
// 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);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
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()
};
let server = IrcServer::from_config(config).unwrap();
server.identify().unwrap();
let client = IrcClient::from_config(config).unwrap();
client.identify().unwrap();
server.for_each_incoming(|message| {
client.for_each_incoming(|message| {
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.starts_with(server.current_nickname()) {
let tokens: Vec<_> = msg.split(' ').collect();
if tokens.len() > 2 {
let n = tokens[0].len() + tokens[1].len() + 2;
if let Ok(count) = tokens[1].parse::<u8>() {
for _ in 0..count {
server.send_privmsg(
message.response_target().unwrap_or(target),
&msg[n..]
).unwrap();
}
if let Command::PRIVMSG(ref target, ref msg) = message.command {
if msg.starts_with(client.current_nickname()) {
let tokens: Vec<_> = msg.split(' ').collect();
if tokens.len() > 2 {
let n = tokens[0].len() + tokens[1].len() + 2;
if let Ok(count) = tokens[1].parse::<u8>() {
for _ in 0..count {
client.send_privmsg(
message.response_target().unwrap_or(target),
&msg[n..]
).unwrap();
}
}
}
}
_ => (),
}
}).unwrap()
}

View file

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

View file

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

View file

@ -12,15 +12,15 @@ fn main() {
channels: Some(vec!["#irc-crate".to_owned()]),
..Default::default()
};
let server = IrcServer::from_config(config).unwrap();
server.identify().unwrap();
let server2 = server.clone();
let client = IrcClient::from_config(config).unwrap();
client.identify().unwrap();
let client2 = client.clone();
// Let's set up a loop that just prints the messages.
thread::spawn(move || {
server2.stream().map(|m| print!("{}", m)).wait().count();
client2.stream().map(|m| print!("{}", m)).wait().count();
});
loop {
server.send_privmsg("#irc-crate", "TWEET TWEET").unwrap();
client.send_privmsg("#irc-crate", "TWEET TWEET").unwrap();
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
cargo run --example convertconf --features "toml yaml" -- -i client_config.json -o client_config.toml
cargo run --example convertconf --features "toml yaml" -- -i client_config.json -o client_config.yaml
echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.test.net\",\"port\": 6667,\"use_ssl\": false,\"encoding\": \"UTF-8\",\"channels\": [\"#test\", \"#test2\"],\"umodes\": \"+BR\",\"options\": {}}" > client_config.json
cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.toml
cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.yaml

View file

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

View file

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

View file

@ -1,28 +1,28 @@
//! 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
//! 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.
//!
//! # Examples
//!
//!
//! Using these APIs, we can connect to a server and send a one-off message (in this case,
//! identifying with the server).
//!
//! ```no_run
//! # extern crate irc;
//! use irc::client::prelude::{IrcServer, ServerExt};
//! use irc::client::prelude::{IrcClient, ClientExt};
//!
//! # fn main() {
//! let server = IrcServer::new("config.toml").unwrap();
//! // identify and send_privmsg both come from `ServerExt`
//! let server = IrcClient::new("config.toml").unwrap();
//! // identify and send_privmsg both come from `ClientExt`
//! server.identify().unwrap();
//! server.send_privmsg("#example", "Hello, world!").unwrap();
//! # }
//! ```
//!
//! `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
//! compatibility). This means that all IRCv3 capability requests should be performed before calling
//! `identify`. For example:
@ -31,7 +31,7 @@
//! # extern crate irc;
//! # use irc::client::prelude::*;
//! # 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.identify().unwrap();
//! # }
@ -46,10 +46,10 @@ use proto::{Capability, Command, Mode, NegotiationVersion};
use proto::command::CapSubCommand::{END, LS, REQ};
use proto::command::Command::*;
use proto::mode::ModeType;
use client::server::Server;
use client::Client;
/// Idiomatic extensions for sending messages to an IRC server.
pub trait ServerExt: Server {
/// Idiomatic extensions for sending messages to an IRC server as a [`Client`](../trait.Client.html).
pub trait ClientExt: Client {
/// Sends a request for a list of server capabilities for a specific IRCv3 version.
fn send_cap_ls(&self, version: NegotiationVersion) -> Result<()>
where
@ -95,7 +95,7 @@ pub trait ServerExt: Server {
if self.config().password() != "" {
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.config().username().to_owned(),
"0".to_owned(),
@ -377,26 +377,22 @@ pub trait ServerExt: Server {
}
}
impl<S> ServerExt for S
where
S: Server,
{
}
impl<C> ClientExt for C where C: Client {}
#[cfg(test)]
mod test {
use super::ServerExt;
use super::ClientExt;
use client::data::Config;
use client::server::IrcServer;
use client::server::test::{get_server_value, test_config};
use client::IrcClient;
use client::test::{get_client_value, test_config};
use proto::{ChannelMode, Mode};
#[test]
fn identify() {
let server = IrcServer::from_config(test_config()).unwrap();
server.identify().unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.identify().unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"CAP END\r\nNICK :test\r\n\
USER test 0 * :test\r\n"
);
@ -404,14 +400,14 @@ mod test {
#[test]
fn identify_with_password() {
let server = IrcServer::from_config(Config {
let client = IrcClient::from_config(Config {
nickname: Some(format!("test")),
password: Some(format!("password")),
..test_config()
}).unwrap();
server.identify().unwrap();
client.identify().unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"CAP END\r\nPASS :password\r\nNICK :test\r\n\
USER test 0 * :test\r\n"
);
@ -419,149 +415,149 @@ mod test {
#[test]
fn send_pong() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_pong("irc.test.net").unwrap();
assert_eq!(&get_server_value(server)[..], "PONG :irc.test.net\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_pong("irc.test.net").unwrap();
assert_eq!(&get_client_value(client)[..], "PONG :irc.test.net\r\n");
}
#[test]
fn send_join() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_join("#test,#test2,#test3").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_join("#test,#test2,#test3").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"JOIN #test,#test2,#test3\r\n"
);
}
#[test]
fn send_part() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_part("#test").unwrap();
assert_eq!(&get_server_value(server)[..], "PART #test\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_part("#test").unwrap();
assert_eq!(&get_client_value(client)[..], "PART #test\r\n");
}
#[test]
fn send_oper() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_oper("test", "test").unwrap();
assert_eq!(&get_server_value(server)[..], "OPER test :test\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_oper("test", "test").unwrap();
assert_eq!(&get_client_value(client)[..], "OPER test :test\r\n");
}
#[test]
fn send_privmsg() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_privmsg("#test", "Hi, everybody!").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_privmsg("#test", "Hi, everybody!").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"PRIVMSG #test :Hi, everybody!\r\n"
);
}
#[test]
fn send_notice() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_notice("#test", "Hi, everybody!").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_notice("#test", "Hi, everybody!").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"NOTICE #test :Hi, everybody!\r\n"
);
}
#[test]
fn send_topic_no_topic() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_topic("#test", "").unwrap();
assert_eq!(&get_server_value(server)[..], "TOPIC #test\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_topic("#test", "").unwrap();
assert_eq!(&get_client_value(client)[..], "TOPIC #test\r\n");
}
#[test]
fn send_topic() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_topic("#test", "Testing stuff.").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_topic("#test", "Testing stuff.").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"TOPIC #test :Testing stuff.\r\n"
);
}
#[test]
fn send_kill() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_kill("test", "Testing kills.").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_kill("test", "Testing kills.").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"KILL test :Testing kills.\r\n"
);
}
#[test]
fn send_kick_no_message() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_kick("#test", "test", "").unwrap();
assert_eq!(&get_server_value(server)[..], "KICK #test test\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_kick("#test", "test", "").unwrap();
assert_eq!(&get_client_value(client)[..], "KICK #test test\r\n");
}
#[test]
fn send_kick() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_kick("#test", "test", "Testing kicks.").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_kick("#test", "test", "Testing kicks.").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"KICK #test test :Testing kicks.\r\n"
);
}
#[test]
fn send_mode_no_modeparams() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap();
assert_eq!(&get_server_value(server)[..], "MODE #test +i\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap();
assert_eq!(&get_client_value(client)[..], "MODE #test +i\r\n");
}
#[test]
fn send_mode() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))])
let client = IrcClient::from_config(test_config()).unwrap();
client.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))])
.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]
fn send_samode_no_modeparams() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_samode("#test", "+i", "").unwrap();
assert_eq!(&get_server_value(server)[..], "SAMODE #test +i\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_samode("#test", "+i", "").unwrap();
assert_eq!(&get_client_value(client)[..], "SAMODE #test +i\r\n");
}
#[test]
fn send_samode() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_samode("#test", "+o", "test").unwrap();
assert_eq!(&get_server_value(server)[..], "SAMODE #test +o test\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_samode("#test", "+o", "test").unwrap();
assert_eq!(&get_client_value(client)[..], "SAMODE #test +o test\r\n");
}
#[test]
fn send_sanick() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_sanick("test", "test2").unwrap();
assert_eq!(&get_server_value(server)[..], "SANICK test test2\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_sanick("test", "test2").unwrap();
assert_eq!(&get_client_value(client)[..], "SANICK test test2\r\n");
}
#[test]
fn send_invite() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_invite("test", "#test").unwrap();
assert_eq!(&get_server_value(server)[..], "INVITE test #test\r\n");
let client = IrcClient::from_config(test_config()).unwrap();
client.send_invite("test", "#test").unwrap();
assert_eq!(&get_client_value(client)[..], "INVITE test #test\r\n");
}
#[test]
#[cfg(feature = "ctcp")]
fn send_ctcp() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_ctcp("test", "MESSAGE").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_ctcp("test", "MESSAGE").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"PRIVMSG test :\u{001}MESSAGE\u{001}\r\n"
);
}
@ -569,10 +565,10 @@ mod test {
#[test]
#[cfg(feature = "ctcp")]
fn send_action() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_action("test", "tests.").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_action("test", "tests.").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n"
);
}
@ -580,10 +576,10 @@ mod test {
#[test]
#[cfg(feature = "ctcp")]
fn send_finger() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_finger("test").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_finger("test").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"PRIVMSG test :\u{001}FINGER\u{001}\r\n"
);
}
@ -591,10 +587,10 @@ mod test {
#[test]
#[cfg(feature = "ctcp")]
fn send_version() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_version("test").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_version("test").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"PRIVMSG test :\u{001}VERSION\u{001}\r\n"
);
}
@ -602,10 +598,10 @@ mod test {
#[test]
#[cfg(feature = "ctcp")]
fn send_source() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_source("test").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_source("test").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"PRIVMSG test :\u{001}SOURCE\u{001}\r\n"
);
}
@ -613,10 +609,10 @@ mod test {
#[test]
#[cfg(feature = "ctcp")]
fn send_user_info() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_user_info("test").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_user_info("test").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"PRIVMSG test :\u{001}USERINFO\u{001}\r\n"
);
}
@ -624,9 +620,9 @@ mod test {
#[test]
#[cfg(feature = "ctcp")]
fn send_ctcp_ping() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_ctcp_ping("test").unwrap();
let val = get_server_value(server);
let client = IrcClient::from_config(test_config()).unwrap();
client.send_ctcp_ping("test").unwrap();
let val = get_client_value(client);
println!("{}", val);
assert!(val.starts_with("PRIVMSG test :\u{001}PING "));
assert!(val.ends_with("\u{001}\r\n"));
@ -635,10 +631,10 @@ mod test {
#[test]
#[cfg(feature = "ctcp")]
fn send_time() {
let server = IrcServer::from_config(test_config()).unwrap();
server.send_time("test").unwrap();
let client = IrcClient::from_config(test_config()).unwrap();
client.send_time("test").unwrap();
assert_eq!(
&get_server_value(server)[..],
&get_client_value(client)[..],
"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.
//!
//! 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) implements provides methods for communicating with this
//! server. An extension trait, [ServerExt](./utils/trait.ServerExt.html), provides short-hand for
//! [`IrcServer`](struct.IrcServer.html) type. The [`Server`](trait.Server.html) trait that
//! [`IrcServer`](struct.IrcServer.html) implements provides methods for communicating with this
//! 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
//! [proto::command](../../proto/command/enum.Command.html).
//! [`proto::command`](../../proto/command/enum.Command.html).
//!
//! # Examples
//!
@ -17,7 +17,7 @@
//! use irc::client::prelude::{IrcServer, ServerExt};
//!
//! # fn main() {
//! let server = IrcServer::new("config.toml").unwrap();
//! let server = IrcServer::new("config.toml").unwrap();
//! // identify comes from `ServerExt`
//! server.identify().unwrap();
//! # }
@ -73,9 +73,9 @@ pub mod utils;
/// 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
/// [Server::for_each_incoming](trait.Server.html#method.for_each_incoming).
/// [`Server::for_each_incoming`](trait.Server.html#method.for_each_incoming).
///
/// # Example
///
@ -85,7 +85,7 @@ pub mod utils;
/// use irc::client::prelude::EachIncomingExt;
///
/// # fn main() {
/// # let server = IrcServer::new("config.toml").unwrap();
/// # let server = IrcServer::new("config.toml").unwrap();
/// # server.identify().unwrap();
/// server.stream().for_each_incoming(|irc_msg| {
/// match irc_msg.command {
@ -97,7 +97,7 @@ pub mod utils;
/// }).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.
fn for_each_incoming<F>(self, mut f: F) -> error::Result<()>
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.
pub trait Server {
@ -128,7 +128,7 @@ pub trait Server {
/// # extern crate irc;
/// # use irc::client::prelude::*;
/// # 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::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap();
/// # }
@ -153,7 +153,7 @@ pub trait Server {
/// # extern crate irc;
/// # use irc::client::prelude::{IrcServer, ServerExt, Server, Command};
/// # fn main() {
/// # let server = IrcServer::new("config.toml").unwrap();
/// # let server = IrcServer::new("config.toml").unwrap();
/// # server.identify().unwrap();
/// server.for_each_incoming(|irc_msg| {
/// match irc_msg.command {
@ -189,7 +189,7 @@ pub trait Server {
/// use irc::proto::caps::Capability;
///
/// # fn main() {
/// # let server = IrcServer::new("config.toml").unwrap();
/// # let server = IrcServer::new("config.toml").unwrap();
/// server.send_cap_req(&[Capability::MultiPrefix]).unwrap();
/// server.identify().unwrap();
/// # }
@ -203,6 +203,7 @@ pub trait Server {
/// traditional use cases. To learn more, you can view the documentation for the
/// [futures](https://docs.rs/futures/) crate, or the tutorials for
/// [tokio](https://tokio.rs/docs/getting-started/futures/).
#[derive(Debug)]
pub struct ServerStream {
state: Arc<ServerState>,
stream: SplitStream<Connection>,
@ -210,7 +211,7 @@ pub struct ServerStream {
impl Stream for ServerStream {
type Item = Message;
type Error = error::Error;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
match try_ready!(self.stream.poll()) {
@ -224,6 +225,7 @@ impl Stream for ServerStream {
}
/// Thread-safe internal state for an IRC server connection.
#[derive(Debug)]
struct ServerState {
/// The configuration used with this connection.
config: Config,
@ -326,7 +328,9 @@ impl ServerState {
let alt_nicks = self.config().alternate_nicknames();
let index = self.alt_nick_index.read().unwrap();
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],
}
}
@ -420,12 +424,12 @@ impl ServerState {
self.send(NICKSERV(format!(
"{} {} {}",
seq,
self.config().nickname(),
self.config().nickname()?,
self.config().nick_password()
)))?;
}
*index = 0;
self.send(NICK(self.config().nickname().to_owned()))?
self.send(NICK(self.config().nickname()?.to_owned()))?
}
self.send(NICKSERV(
format!("IDENTIFY {}", self.config().nick_password()),
@ -437,7 +441,16 @@ impl ServerState {
if self.config().umodes().is_empty() {
Ok(())
} 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
/// 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
/// server after connection. Cloning an `IrcServer` is relatively cheap, as it's equivalent to
/// [`ServerExt`](./utils/trait.ServerExt.html) traits that provide methods of communicating with
/// 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
/// connection.
#[derive(Clone)]
///
/// For a full example usage, see [`irc::client::server`](./index.html).
#[derive(Clone, Debug)]
pub struct IrcServer {
/// The internal, thread-safe server state.
state: Arc<ServerState>,
@ -667,7 +682,7 @@ impl IrcServer {
/// # extern crate irc;
/// # use irc::client::prelude::*;
/// # 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> {
@ -678,7 +693,8 @@ impl IrcServer {
/// immediately. Due to current design limitations, error handling here is somewhat limited. In
/// particular, failed connections will cause the program to panic because the connection
/// attempt is made on a freshly created thread. If you need to avoid this behavior and handle
/// errors more gracefully, it is recommended that you use `IrcServer::new_future` instead.
/// errors more gracefully, it is recommended that you use an
/// [IrcReactor](../reactor/struct.IrcReactor.html) instead.
///
/// # Example
/// ```no_run
@ -691,7 +707,7 @@ impl IrcServer {
/// server: Some("irc.example.com".to_owned()),
/// .. 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> {
@ -700,22 +716,21 @@ impl IrcServer {
let (tx_incoming, rx_incoming) = 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 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();
let (sink, stream) = conn.split();
let outgoing_future = sink.send_all(rx_outgoing.map_err(|_| {
let res: error::Error = error::ErrorKind::ChannelError.into();
res
let outgoing_future = sink.send_all(rx_outgoing.map_err::<error::IrcError, _>(|_| {
unreachable!("futures::sync::mpsc::Receiver should never return Err");
})).map(|_| ()).map_err(|e| panic!("{}", e));
// 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
/// in the crate documentation for [tokio-core](http://docs.rs/tokio-core) or
/// [futures](http://docs.rs/futures). Additionally, you can find detailed tutorials on using
/// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/).
/// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). An easy
/// to use abstraction that does not require this knowledge is available via
/// [`IrcReactors`](../reactor/struct.IrcReactor.html).
///
/// # Example
/// ```no_run
@ -746,6 +763,7 @@ impl IrcServer {
/// # extern crate tokio_core;
/// # use std::default::Default;
/// # use irc::client::prelude::*;
/// # use irc::client::server::PackedIrcServer;
/// # use irc::error;
/// # use tokio_core::reactor::Core;
/// # fn main() {
@ -755,14 +773,14 @@ impl IrcServer {
/// # .. Default::default()
/// # };
/// 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...
/// 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...
/// reactor.run(server.stream().for_each(move |irc_msg| {
/// // processing messages works like usual
/// process_msg(&server, irc_msg)
/// })).unwrap();
/// }).join(future)).unwrap();
/// # }
/// # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) }
/// ```
@ -771,7 +789,7 @@ impl IrcServer {
Ok(IrcServerFuture {
conn: Connection::new(config, &handle)?,
handle: handle,
_handle: handle,
config: config,
tx_outgoing: Some(tx_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
/// use cases. To learn more, you can view the documentation for the
/// [futures](https://docs.rs/futures/) crate, or the tutorials for
/// [tokio](https://tokio.rs/docs/getting-started/futures/).
/// [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> {
conn: ConnectionFuture<'a>,
handle: Handle,
_handle: Handle,
config: &'a Config,
tx_outgoing: Option<UnboundedSender<Message>>,
rx_outgoing: Option<UnboundedReceiver<Message>>,
}
impl<'a> Future for IrcServerFuture<'a> {
type Item = IrcServer;
type Error = error::Error;
type Item = PackedIrcServer;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
let conn = try_ready!(self.conn.poll());
@ -816,22 +836,29 @@ impl<'a> Future for IrcServerFuture<'a> {
let view = conn.log_view();
let (sink, stream) = conn.split();
let outgoing_future = sink.send_all(self.rx_outgoing.take().unwrap().map_err(|()| {
let res: error::Error = error::ErrorKind::ChannelError.into();
res
})).map(|_| ()).map_err(|e| panic!(e));
let outgoing_future = sink.send_all(
self.rx_outgoing.take().unwrap().map_err::<error::IrcError, _>(|()| {
unreachable!("futures::sync::mpsc::Receiver should never return Err");
})
).map(|_| ());
self.handle.spawn(outgoing_future);
Ok(Async::Ready(IrcServer {
let server = IrcServer {
state: Arc::new(ServerState::new(
stream, self.tx_outgoing.take().unwrap(), self.config.clone()
)),
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)]
mod test {

View file

@ -44,11 +44,11 @@ where
inner: inner,
burst_timer: tokio_timer::wheel().build(),
rolling_burst_window: VecDeque::new(),
burst_window_length: config.burst_window_length() as u64,
max_burst_messages: config.max_messages_in_burst() as u64,
burst_window_length: u64::from(config.burst_window_length()),
max_burst_messages: u64::from(config.max_messages_in_burst()),
current_burst_messages: 0,
ping_timer: timer.interval(Duration::from_secs(config.ping_time() as u64)),
ping_timeout: config.ping_timeout() as u64,
ping_timer: timer.interval(Duration::from_secs(u64::from(config.ping_time()))),
ping_timeout: u64::from(config.ping_timeout()),
last_ping_data: String::new(),
last_ping_sent: Instant::now(),
last_pong_received: Instant::now(),
@ -88,12 +88,12 @@ where
T: AsyncRead + AsyncWrite,
{
type Item = Message;
type Error = error::Error;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
if self.ping_timed_out() {
self.close()?;
return Err(error::ErrorKind::PingTimeout.into())
return Err(error::IrcError::PingTimeout)
}
let timer_poll = self.ping_timer.poll()?;
@ -144,12 +144,12 @@ where
T: AsyncRead + AsyncWrite,
{
type SinkItem = Message;
type SinkError = error::Error;
type SinkError = error::IrcError;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
if self.ping_timed_out() {
self.close()?;
Err(error::ErrorKind::PingTimeout.into())
Err(error::IrcError::PingTimeout)
} else {
// Check if the oldest message in the rolling window is discounted.
if let Async::Ready(()) = self.rolling_burst_window_front()? {
@ -180,7 +180,7 @@ where
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
if self.ping_timed_out() {
self.close()?;
Err(error::ErrorKind::PingTimeout.into())
Err(error::IrcError::PingTimeout)
} else {
Ok(self.inner.poll_complete()?)
}
@ -201,16 +201,12 @@ pub struct LogView {
impl LogView {
/// Gets a read guard for all the messages sent on the transport.
pub fn sent(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> {
self.sent.read().map_err(
|_| error::ErrorKind::PoisonedLog.into(),
)
self.sent.read().map_err(|_| error::IrcError::PoisonedLog)
}
/// Gets a read guard for all the messages received on the transport.
pub fn received(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> {
self.received.read().map_err(
|_| error::ErrorKind::PoisonedLog.into(),
)
self.received.read().map_err(|_| error::IrcError::PoisonedLog)
}
}
@ -250,13 +246,13 @@ where
T: AsyncRead + AsyncWrite,
{
type Item = Message;
type Error = error::Error;
type Error = error::IrcError;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
match try_ready!(self.inner.poll()) {
Some(msg) => {
let recv: error::Result<_> = self.view.received.write().map_err(|_| {
error::ErrorKind::PoisonedLog.into()
error::IrcError::PoisonedLog
});
recv?.push(msg.clone());
Ok(Async::Ready(Some(msg)))
@ -271,12 +267,12 @@ where
T: AsyncRead + AsyncWrite,
{
type SinkItem = Message;
type SinkError = error::Error;
type SinkError = error::IrcError;
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
let res = self.inner.start_send(item.clone())?;
let sent: error::Result<_> = self.view.sent.write().map_err(|_| {
error::ErrorKind::PoisonedLog.into()
error::IrcError::PoisonedLog
});
sent?.push(item);
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! {
foreign_links {
Io(::std::io::Error);
Tls(::native_tls::Error);
Recv(::std::sync::mpsc::RecvError);
SendMessage(::futures::sync::mpsc::SendError<::proto::Message>);
OneShotCancelled(::futures::sync::oneshot::Canceled);
Timer(::tokio_timer::TimerError);
}
use futures::sync::mpsc::SendError;
use futures::sync::oneshot::Canceled;
use native_tls::Error as TlsError;
#[cfg(feature = "json")]
use serde_json::Error as JsonError;
#[cfg(feature = "yaml")]
use serde_yaml::Error as YamlError;
use tokio_timer::TimerError;
#[cfg(feature = "toml")]
use toml::de::Error as TomlReadError;
#[cfg(feature = "toml")]
use toml::ser::Error as TomlWriteError;
errors {
/// 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.")
}
use proto::Message;
/// A parsing error for invalid or missing commands in messages.
InvalidCommand {
description("Message contained a missing or invalid Command.")
display("Message contained a missing or invalid Command.")
}
/// A specialized `Result` type for the `irc` crate.
pub type Result<T> = ::std::result::Result<T, IrcError>;
/// A parsing error for failures in subcommand parsing (e.g. CAP and metadata).
SubCommandParsingFailed {
description("Failed to parse an IRC subcommand.")
display("Failed to parse an IRC subcommand.")
}
/// The main crate-wide error type.
#[derive(Debug, Fail)]
pub enum IrcError {
/// An internal I/O error.
#[fail(display = "an io error occurred")]
Io(#[cause] IoError),
/// Failed to parse a mode correctly.
ModeParsingFailed {
description("Failed to parse a mode correctly.")
display("Failed to parse a mode correctly.")
}
/// An internal TLS error.
#[fail(display = "a TLS error occurred")]
Tls(#[cause] TlsError),
/// An error occurred on one of the internal channels of the `IrcServer`.
ChannelError {
description("An error occured on one of the IrcServer's internal channels.")
display("An error occured on one of the IrcServer's internal channels.")
}
/// An internal synchronous channel closed.
#[fail(display = "a sync channel closed")]
SyncChannelClosed(#[cause] RecvError),
/// An error occured causing a mutex for a logged transport to be poisoned.
PoisonedLog {
description("An error occured causing a mutex for a logged transport to be poisoned.")
display("An error occured causing a mutex for a logged transport to be poisoned.")
}
/// An internal asynchronous channel closed.
#[fail(display = "an async channel closed")]
AsyncChannelClosed(#[cause] SendError<Message>),
/// Connection timed out due to no ping response.
PingTimeout {
description("The connection timed out due to no ping response.")
display("The connection timed out due to no ping response.")
}
/// An internal oneshot channel closed.
#[fail(display = "a oneshot channel closed")]
OneShotCanceled(#[cause] Canceled),
/// An internal timer error.
#[fail(display = "timer failed")]
Timer(#[cause] TimerError),
/// Error for invalid configurations.
#[fail(display = "invalid config: {}", path)]
InvalidConfig {
/// 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.
//!
//! # Quick Start
//! The main public API is entirely exported in [client::prelude](./client/prelude/index.html). This
//! should include everything necessary to write an IRC client or bot.
//!
//! The main public API is entirely exported in [`client::prelude`](./client/prelude/index.html).
//! This should include everything necessary to write an IRC client or bot.
//!
//! # A Whirlwind Tour
//! 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
//! client-side functionality, while the proto module features general components of an IRC protocol
//! implementation that could in principle be used in either client or server software. Both modules
//! feature a number of components that are low-level and can be used to build alternative APIs for
//! the IRC protocol. For the average user, the higher-level components for an IRC client are all
//! re-exported in [client::prelude](./client/prelude/index.html). That module serves as the best
//! starting point for a new user trying to understand the high-level API.
//! 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 client-side functionality, while the `proto` module features general components of an IRC
//! protocol implementation that could in principle be used in either client or server software.
//! Both modules feature a number of components that are low-level and can be used to build
//! alternative APIs for the IRC protocol. For the average user, the higher-level components for an
//! IRC client are all re-exported in [`client::prelude`](./client/prelude/index.html). That module
//! serves as the best starting point for a new user trying to understand the high-level API.
//!
//! # Example
//!
@ -22,31 +22,29 @@
//!
//! # fn main() {
//! // configuration is loaded from config.toml into a Config
//! let server = IrcServer::new("config.toml").unwrap();
//! // identify comes from ServerExt
//! server.identify().unwrap();
//! // for_each_incoming comes from Server
//! server.for_each_incoming(|irc_msg| {
//! // irc_msg is a Message
//! match irc_msg.command {
//! Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) {
//! // send_privmsg comes from ServerExt
//! server.send_privmsg(&channel, "beep boop").unwrap();
//! let client = IrcClient::new("config.toml").unwrap();
//! // identify comes from ClientExt
//! client.identify().unwrap();
//! // for_each_incoming comes from Client
//! client.for_each_incoming(|irc_msg| {
//! // irc_msg is a Message
//! if let Command::PRIVMSG(channel, message) = irc_msg.command {
//! if message.contains(client.current_nickname()) {
//! // send_privmsg comes from ClientExt
//! client.send_privmsg(&channel, "beep boop").unwrap();
//! }
//! }
//! _ => ()
//! }
//! }).unwrap();
//! # }
//! ```
#![warn(missing_docs)]
#![recursion_limit="128"]
extern crate bufstream;
extern crate bytes;
extern crate chrono;
#[macro_use]
extern crate error_chain;
extern crate failure;
extern crate encoding;
#[macro_use]
extern crate futures;
@ -72,7 +70,7 @@ pub mod client;
pub mod error;
pub mod proto;
const VERSION_STR: &'static str = concat!(
const VERSION_STR: &str = concat!(
env!("CARGO_PKG_NAME"),
":",
env!("CARGO_PKG_VERSION"),

View file

@ -2,7 +2,7 @@
use std::ascii::AsciiExt;
use std::str::FromStr;
use error;
use error::MessageParseError;
use proto::{ChannelExt, ChannelMode, Mode, Response, UserMode};
/// 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 {
/// 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") {
match suffix {
Some(suffix) => {
@ -1653,8 +1653,9 @@ impl CapSubCommand {
}
impl FromStr for CapSubCommand {
type Err = error::Error;
fn from_str(s: &str) -> error::Result<CapSubCommand> {
type Err = MessageParseError;
fn from_str(s: &str) -> Result<CapSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("LS") {
Ok(CapSubCommand::LS)
} else if s.eq_ignore_ascii_case("LIST") {
@ -1672,7 +1673,10 @@ impl FromStr for CapSubCommand {
} else if s.eq_ignore_ascii_case("DEL") {
Ok(CapSubCommand::DEL)
} 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 {
type Err = error::Error;
fn from_str(s: &str) -> error::Result<MetadataSubCommand> {
type Err = MessageParseError;
fn from_str(s: &str) -> Result<MetadataSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("GET") {
Ok(MetadataSubCommand::GET)
} else if s.eq_ignore_ascii_case("LIST") {
@ -1715,7 +1720,10 @@ impl FromStr for MetadataSubCommand {
} else if s.eq_ignore_ascii_case("CLEAR") {
Ok(MetadataSubCommand::CLEAR)
} 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 {
type Err = error::Error;
fn from_str(s: &str) -> error::Result<BatchSubCommand> {
type Err = MessageParseError;
fn from_str(s: &str) -> Result<BatchSubCommand, Self::Err> {
if s.eq_ignore_ascii_case("NETSPLIT") {
Ok(BatchSubCommand::NETSPLIT)
} else if s.eq_ignore_ascii_case("NETJOIN") {

View file

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

View file

@ -28,7 +28,7 @@ impl LineCodec {
impl Decoder for LineCodec {
type Item = String;
type Error = error::Error;
type Error = error::IrcError;
fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<String>> {
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 {
type Item = String;
type Error = error::Error;
type Error = error::IrcError;
fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> {
// 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 error;
use error::{Error, ErrorKind};
use error::{IrcError, MessageParseError};
use proto::{Command, ChannelExt};
/// A data structure representing an IRC message according to the protocol specification. It
@ -43,7 +43,7 @@ impl Message {
command: &str,
args: Vec<&str>,
suffix: Option<&str>,
) -> error::Result<Message> {
) -> Result<Message, MessageParseError> {
Message::with_tags(None, prefix, command, args, suffix)
}
@ -56,7 +56,7 @@ impl Message {
command: &str,
args: Vec<&str>,
suffix: Option<&str>,
) -> error::Result<Message> {
) -> Result<Message, error::MessageParseError> {
Ok(Message {
tags: tags,
prefix: prefix.map(|s| s.to_owned()),
@ -85,7 +85,7 @@ impl Message {
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, None, None) => Some(s), // <nick>
_ => None, // <servername>
@ -170,13 +170,18 @@ impl From<Command> for Message {
}
impl FromStr for Message {
type Err = Error;
type Err = IrcError;
fn from_str(s: &str) -> Result<Message, Self::Err> {
let mut state = s;
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 = state.find(' ').map(|i| &state[1..i]);
state = state.find(' ').map_or("", |i| &state[i + 1..]);
@ -193,6 +198,7 @@ impl FromStr for Message {
} else {
None
};
let prefix = if state.starts_with(':') {
let prefix = state.find(' ').map(|i| &state[1..i]);
state = state.find(' ').map_or("", |i| &state[i + 1..]);
@ -200,6 +206,7 @@ impl FromStr for Message {
} else {
None
};
let line_ending_len = if state.ends_with("\r\n") {
"\r\n"
} else if state.ends_with('\r') {
@ -209,6 +216,7 @@ impl FromStr for Message {
} else {
""
}.len();
let suffix = if state.contains(" :") {
let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - line_ending_len]);
state = state.find(" :").map_or("", |i| &state[..i + 1]);
@ -217,24 +225,33 @@ impl FromStr for Message {
state = &state[..state.len() - line_ending_len];
None
};
let command = match state.find(' ').map(|i| &state[..i]) {
Some(cmd) => {
state = state.find(' ').map_or("", |i| &state[i + 1..]);
cmd
}
// 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.
None => {
let cmd = state;
state = "";
cmd
},
};
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.
use std::fmt;
use error;
use error::MessageParseError;
use error::MessageParseError::InvalidModeString;
use error::ModeParseError::*;
use proto::Command;
/// A marker trait for different kinds of Modes.
@ -48,10 +50,10 @@ impl ModeType for UserMode {
}
impl UserMode {
fn from_char(c: char) -> error::Result<UserMode> {
fn from_char(c: char) -> UserMode {
use self::UserMode::*;
Ok(match c {
match c {
'a' => Away,
'i' => Invisible,
'w' => Wallops,
@ -61,7 +63,7 @@ impl UserMode {
's' => ServerNotices,
'x' => MaskedHost,
_ => Unknown(c),
})
}
}
}
@ -141,10 +143,10 @@ impl ModeType for ChannelMode {
}
impl ChannelMode {
fn from_char(c: char) -> error::Result<ChannelMode> {
fn from_char(c: char) -> ChannelMode {
use self::ChannelMode::*;
Ok(match c {
match c {
'b' => Ban,
'e' => Exception,
'l' => Limit,
@ -162,7 +164,7 @@ impl ChannelMode {
'h' => Halfop,
'v' => Voice,
_ => Unknown(c),
})
}
}
}
@ -242,7 +244,7 @@ enum PlusMinus {
impl Mode<UserMode> {
// TODO: turning more edge cases into errors.
/// 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::*;
let mut res = vec![];
@ -255,11 +257,18 @@ impl Mode<UserMode> {
let init = match chars.next() {
Some('+') => Plus,
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 {
let mode = UserMode::from_char(c)?;
let mode = UserMode::from_char(c);
let arg = if mode.takes_arg() {
pieces.next()
} else {
@ -281,7 +290,7 @@ impl Mode<UserMode> {
impl Mode<ChannelMode> {
// TODO: turning more edge cases into errors.
/// 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::*;
let mut res = vec![];
@ -294,11 +303,18 @@ impl Mode<ChannelMode> {
let init = match chars.next() {
Some('+') => Plus,
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 {
let mode = ChannelMode::from_char(c)?;
let mode = ChannelMode::from_char(c);
let arg = if mode.takes_arg() {
pieces.next()
} else {