commit
a1b271c167
30 changed files with 2432 additions and 724 deletions
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
18
README.md
18
README.md
|
@ -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`.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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!")?;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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
31
examples/reactor.rs
Normal 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
57
examples/reconnector.rs
Normal 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(())
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
1360
src/client/mod.rs
1360
src/client/mod.rs
File diff suppressed because it is too large
Load diff
31
src/client/prelude.rs
Normal file
31
src/client/prelude.rs
Normal 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
191
src/client/reactor.rs
Normal 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(|_| ()))
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
269
src/error.rs
269
src/error.rs
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
50
src/lib.rs
50
src/lib.rs
|
@ -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"),
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue