Merge pull request #154 from aatxe/develop

Finalize 0.13.6.
This commit is contained in:
Aaron Weiss 2018-10-03 12:53:14 -04:00 committed by GitHub
commit 0f6f72d4c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 560 additions and 136 deletions

View file

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

View file

@ -10,6 +10,14 @@ possible for everyone, there are a few guidelines that we need contributors to f
* Make sure you have a [GitHub account](https://github.com/signup/free). * Make sure you have a [GitHub account](https://github.com/signup/free).
* Fork the repository on GitHub. * Fork the repository on GitHub.
## Finding Something to Do
There will almost certainly be issues open on the irc crate at all times. These are a good place to
start if you don't have anything pressing on your mind that you'd like to see. If you need or would
like mentoring instructions on any issue, please ping [@aatxe](https://github.com/aatxe), and he
will do his best to provide them in a timely fashion. If instead you have your own idea and would
like mentoring or feedback, you should open a new issue and mention such a desire.
## Making Changes ## Making Changes
* Create a topic branch from where you want to base your work. * Create a topic branch from where you want to base your work.

View file

@ -1,10 +1,10 @@
[package] [package]
name = "irc" name = "irc"
version = "0.13.5" version = "0.13.6"
description = "A simple, thread-safe, and async-friendly library for IRC clients." description = "the irc crate usable, async IRC for Rust "
authors = ["Aaron Weiss <awe@pdgn.co>"] authors = ["Aaron Weiss <awe@pdgn.co>"]
license = "MPL-2.0" license = "MPL-2.0"
keywords = ["irc", "client", "thread-safe", "async", "tokio"] keywords = ["irc", "client", "thread-safe", "async", "tokio", "protocol"]
categories = ["asynchronous", "network-programming"] categories = ["asynchronous", "network-programming"]
documentation = "https://docs.rs/irc/" documentation = "https://docs.rs/irc/"
repository = "https://github.com/aatxe/irc" repository = "https://github.com/aatxe/irc"
@ -12,6 +12,8 @@ readme = "README.md"
[badges] [badges]
travis-ci = { repository = "aatxe/irc" } travis-ci = { repository = "aatxe/irc" }
is-it-maintained-issue-resolution = { repository = "aatxe/irc" }
is-it-maintained-open-issues = { repository = "aatxe/irc" }
[features] [features]
default = ["ctcp", "toml"] default = ["ctcp", "toml"]
@ -27,17 +29,18 @@ chrono = "0.4"
encoding = "0.2" encoding = "0.2"
failure = "0.1" failure = "0.1"
futures = "0.1" futures = "0.1"
log = "0.3" log = "0.4"
native-tls = "0.1" native-tls = "0.2"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = { version = "1.0", optional = true } serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.7", optional = true } serde_yaml = { version = "0.7", optional = true }
tokio-codec = "0.1"
tokio-core = "0.1" tokio-core = "0.1"
tokio-io = "0.1" tokio-io = "0.1"
tokio-mockstream = "1.1" tokio-mockstream = "1.1"
tokio-timer = "0.1" tokio-timer = "0.1"
tokio-tls = "0.1" tokio-tls = "0.2"
toml = { version = "0.4", optional = true } toml = { version = "0.4", optional = true }
[dev-dependencies] [dev-dependencies]

View file

@ -1,13 +1,12 @@
# the irc crate [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] [![Docs][doc-badge]][doc] [![Built with Spacemacs][bws]][sm] # the irc crate [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] ![Downloads][dl-badge] [![Docs][doc-badge]][doc]
[ci-badge]: https://travis-ci.org/aatxe/irc.svg?branch=stable [ci-badge]: https://travis-ci.org/aatxe/irc.svg?branch=stable
[ci]: https://travis-ci.org/aatxe/irc [ci]: https://travis-ci.org/aatxe/irc
[cr-badge]: https://img.shields.io/crates/v/irc.svg [cr-badge]: https://img.shields.io/crates/v/irc.svg
[cr]: https://crates.io/crates/irc [cr]: https://crates.io/crates/irc
[dl-badge]: https://img.shields.io/crates/d/irc.svg
[doc-badge]: https://docs.rs/irc/badge.svg [doc-badge]: https://docs.rs/irc/badge.svg
[doc]: https://docs.rs/irc [doc]: https://docs.rs/irc
[bws]: https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg
[sm]: http://spacemacs.org
[rfc2812]: http://tools.ietf.org/html/rfc2812 [rfc2812]: http://tools.ietf.org/html/rfc2812
[ircv3.1]: http://ircv3.net/irc/3.1.html [ircv3.1]: http://ircv3.net/irc/3.1.html
@ -18,7 +17,7 @@ compliant with [RFC 2812][rfc2812], [IRCv3.1][ircv3.1], [IRCv3.2][ircv3.2], and
additional, common features from popular IRCds. You can find up-to-date, ready-to-use documentation additional, common features from popular IRCds. You can find up-to-date, ready-to-use documentation
online [on docs.rs][doc]. online [on docs.rs][doc].
## Built with the irc crate ## ## Built with the irc crate
the irc crate is being used to build new IRC software in Rust. Here are some of our favorite the irc crate is being used to build new IRC software in Rust. Here are some of our favorite
projects: projects:
@ -26,26 +25,32 @@ projects:
- [alectro][alectro] — a terminal IRC client - [alectro][alectro] — a terminal IRC client
- [spilo][spilo] — a minimalistic IRC bouncer - [spilo][spilo] — a minimalistic IRC bouncer
- [irc-bot.rs][ircbot] — a library for writing IRC bots - [irc-bot.rs][ircbot] — a library for writing IRC bots
- [playbot_ng][playbot_ng] — a Rust-evaluating IRC bot in Rust
- [bunnybutt-rs][bunnybutt] — an IRC bot for the [Feed The Beast Wiki][ftb-wiki] - [bunnybutt-rs][bunnybutt] — an IRC bot for the [Feed The Beast Wiki][ftb-wiki]
- [url-bot-rs][url-bot-rs] — a URL-fetching IRC bot
[alectro]: https://github.com/aatxe/alectro [alectro]: https://github.com/aatxe/alectro
[spilo]: https://github.com/aatxe/spilo [spilo]: https://github.com/aatxe/spilo
[ircbot]: https://github.com/8573/irc-bot.rs [ircbot]: https://github.com/8573/irc-bot.rs
[bunnybutt]: https://github.com/FTB-Gamepedia/bunnybutt-rs [bunnybutt]: https://github.com/FTB-Gamepedia/bunnybutt-rs
[playbot_ng]: https://github.com/panicbit/playbot_ng
[ftb-wiki]: https://ftb.gamepedia.com/FTB_Wiki [ftb-wiki]: https://ftb.gamepedia.com/FTB_Wiki
[url-bot-rs]: https://github.com/nuxeh/url-bot-rs
Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pulls) to add it! Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pulls) to add it!
## Getting Started ## ## Getting Started
To start using the irc crate with cargo, you can simply add `irc = "0.13"` to your dependencies in To start using the irc crate with cargo, you can add `irc = "0.13"` to your dependencies in
your Cargo.toml file. The high-level API can be found `irc::client::prelude` linked to from the your Cargo.toml file. The high-level API can be found in [`irc::client::prelude`][irc-prelude].
[doc root][doc]. You'll find a number of examples in `examples/`, throughout the documentation, and You'll find a number of examples to help you get started in `examples/`, throughout the
below. documentation, and below.
## A Tale of Two APIs ## [irc-prelude]: https://docs.rs/irc/*/irc/client/prelude/index.html
### Reactors (The "New" API) ### ## A Tale of Two APIs
### Reactors (The "New" API)
The release of v0.13 brought with it a new API called `IrcReactor` that enables easier multiserver The release of v0.13 brought with it a new API called `IrcReactor` that enables easier multiserver
support and more graceful error handling. The general model is that you use the reactor to create support and more graceful error handling. The general model is that you use the reactor to create
@ -81,7 +86,7 @@ fn main() {
``` ```
### Direct Style (The "Old" API) ### ### Direct Style (The "Old" API)
The old API for connecting to an IRC server is still supported through the `IrcClient` type. It's The old API for connecting to an IRC server is still supported through the `IrcClient` type. It's
simpler for the most basic use case, but will panic upon encountering any sort of connection issues. simpler for the most basic use case, but will panic upon encountering any sort of connection issues.
@ -113,7 +118,7 @@ fn main() {
} }
``` ```
## Configuring IRC Clients ## ## Configuring IRC Clients
As seen above, there are two techniques for configuring the irc crate: runtime loading and As seen above, there are two techniques for configuring the irc crate: runtime loading and
programmatic configuration. Runtime loading is done via the function `Config::load`, and is likely programmatic configuration. Runtime loading is done via the function `Config::load`, and is likely
@ -122,8 +127,9 @@ also be useful when defining your own custom configuration format that can be co
The primary configuration format is TOML, but if you are so inclined, you can use JSON and/or YAML The primary configuration format is TOML, but if you are so inclined, you can use JSON and/or YAML
via the optional `json` and `yaml` features respectively. At the minimum, a configuration requires via the optional `json` and `yaml` features respectively. At the minimum, a configuration requires
`nickname` and `server` to be defined, and all other fields are optional. You can find detailed `nickname` and `server` to be defined, and all other fields are optional. You can find detailed
explanations of the various fields on explanations of the various fields on [docs.rs][config-fields].
[docs.rs](https://docs.rs/irc/0.13.2/irc/client/data/config/struct.Config.html#fields).
[config-fields]: https://docs.rs/irc/*/irc/client/data/config/struct.Config.html#fields
Alternatively, you can look at the example below of a TOML configuration with all the fields: Alternatively, you can look at the example below of a TOML configuration with all the fields:
@ -139,6 +145,8 @@ port = 6697
password = "" password = ""
use_ssl = true use_ssl = true
cert_path = "cert.der" cert_path = "cert.der"
client_cert_path = "client.der"
client_cert_pass = "password"
encoding = "UTF-8" encoding = "UTF-8"
channels = ["#rust", "#haskell", "#fake"] channels = ["#rust", "#haskell", "#fake"]
umodes = "+RB-x" umodes = "+RB-x"
@ -168,9 +176,9 @@ cargo run --example convertconf -- -i client_config.json -o client_config.toml
``` ```
Note that the formats are automatically determined based on the selected file extensions. This Note that the formats are automatically determined based on the selected file extensions. This
tool should make it easy for users to migrate their old configurations to TOML. tool should make it easier for users to migrate their old configurations to TOML.
## Contributing ## ## Contributing
the irc crate is a free, open source library that relies on contributions from its maintainers, the irc crate is a free, open source library that relies on contributions from its maintainers,
Aaron Weiss ([@aatxe][awe]) and Peter Atashian ([@retep998][bun]), as well as the broader Rust Aaron Weiss ([@aatxe][awe]) and Peter Atashian ([@retep998][bun]), as well as the broader Rust
community. It's licensed under the Mozilla Public License 2.0 whose text can be found in community. It's licensed under the Mozilla Public License 2.0 whose text can be found in

View file

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

View file

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

View file

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

View file

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

View file

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

51
examples/tooter.rs Normal file
View file

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

View file

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

View file

@ -6,12 +6,12 @@ use std::io::Read;
use encoding::EncoderTrap; use encoding::EncoderTrap;
use encoding::label::encoding_from_whatwg_label; use encoding::label::encoding_from_whatwg_label;
use futures::{Async, Poll, Future, Sink, StartSend, Stream}; use futures::{Async, Poll, Future, Sink, StartSend, Stream};
use native_tls::{Certificate, TlsConnector}; use native_tls::{Certificate, TlsConnector, Identity};
use tokio_codec::Decoder;
use tokio_core::reactor::Handle; use tokio_core::reactor::Handle;
use tokio_core::net::{TcpStream, TcpStreamNew}; use tokio_core::net::{TcpStream, TcpStreamNew};
use tokio_io::AsyncRead;
use tokio_mockstream::MockStream; use tokio_mockstream::MockStream;
use tokio_tls::{TlsConnectorExt, TlsStream}; use tokio_tls::{self, TlsStream};
use error; use error;
use client::data::Config; use client::data::Config;
@ -81,13 +81,15 @@ impl<'a> Future for ConnectionFuture<'a> {
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
match *self { match *self {
ConnectionFuture::Unsecured(config, ref mut inner) => { ConnectionFuture::Unsecured(config, ref mut inner) => {
let framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?); let stream = try_ready!(inner.poll());
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = IrcTransport::new(config, framed); let transport = IrcTransport::new(config, framed);
Ok(Async::Ready(Connection::Unsecured(transport))) Ok(Async::Ready(Connection::Unsecured(transport)))
} }
ConnectionFuture::Secured(config, ref mut inner) => { ConnectionFuture::Secured(config, ref mut inner) => {
let framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?); let stream = try_ready!(inner.poll());
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = IrcTransport::new(config, framed); let transport = IrcTransport::new(config, framed);
Ok(Async::Ready(Connection::Secured(transport))) Ok(Async::Ready(Connection::Secured(transport)))
@ -109,7 +111,8 @@ impl<'a> Future for ConnectionFuture<'a> {
}) })
}; };
let framed = MockStream::new(&initial?).framed(IrcCodec::new(config.encoding())?); let stream = MockStream::new(&initial?);
let framed = IrcCodec::new(config.encoding())?.framed(stream);
let transport = IrcTransport::new(config, framed); let transport = IrcTransport::new(config, framed);
Ok(Async::Ready(Connection::Mock(Logged::wrap(transport)))) Ok(Async::Ready(Connection::Mock(Logged::wrap(transport))))
@ -126,21 +129,30 @@ impl Connection {
} else if config.use_ssl() { } else if config.use_ssl() {
let domain = format!("{}", config.server()?); let domain = format!("{}", config.server()?);
info!("Connecting via SSL to {}.", domain); info!("Connecting via SSL to {}.", domain);
let mut builder = TlsConnector::builder()?; let mut builder = TlsConnector::builder();
if let Some(cert_path) = config.cert_path() { if let Some(cert_path) = config.cert_path() {
let mut file = File::open(cert_path)?; let mut file = File::open(cert_path)?;
let mut cert_data = vec![]; let mut cert_data = vec![];
file.read_to_end(&mut cert_data)?; file.read_to_end(&mut cert_data)?;
let cert = Certificate::from_der(&cert_data)?; let cert = Certificate::from_der(&cert_data)?;
builder.add_root_certificate(cert)?; builder.add_root_certificate(cert);
info!("Added {} to trusted certificates.", cert_path); info!("Added {} to trusted certificates.", cert_path);
} }
let connector = builder.build()?; if let Some(client_cert_path) = config.client_cert_path() {
let client_cert_pass = config.client_cert_pass();
let mut file = File::open(client_cert_path)?;
let mut client_cert_data = vec![];
file.read_to_end(&mut client_cert_data)?;
let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?;
builder.identity(pkcs12_archive);
info!("Using {} for client certificate authentication.", client_cert_path);
}
let connector: tokio_tls::TlsConnector = builder.build()?.into();
let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle).map_err(|e| { let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle).map_err(|e| {
let res: error::IrcError = e.into(); let res: error::IrcError = e.into();
res res
}).and_then(move |socket| { }).and_then(move |socket| {
connector.connect_async(&domain, socket).map_err( connector.connect(&domain, socket).map_err(
|e| e.into(), |e| e.into(),
) )
})); }));

View file

@ -18,7 +18,51 @@ use error::TomlError;
use error::{ConfigError, Result}; use error::{ConfigError, Result};
use error::IrcError::InvalidConfig; use error::IrcError::InvalidConfig;
/// Configuration data. /// Configuration for IRC clients.
///
/// # Building a configuration programmatically
///
/// For some use cases, it may be useful to build configurations programmatically. Since `Config` is
/// an ordinary struct with public fields, this should be rather straightforward. However, it is
/// important to note that the use of `Config::default()` is important, even when specifying all
/// visible fields because `Config` keeps track of whether it was loaded from a file or
/// programmatically defined, in order to produce better error messages. Using `Config::default()`
/// as below will ensure that this process is handled correctly.
///
/// ```
/// # extern crate irc;
/// use irc::client::prelude::Config;
///
/// # fn main() {
/// let config = Config {
/// nickname: Some("test".to_owned()),
/// server: Some("irc.example.com".to_owned()),
/// ..Config::default()
/// };
/// # }
/// ```
///
/// # Loading a configuration from a file
///
/// The standard method of using a configuration is to load it from a TOML file. You can find an
/// example TOML configuration in the README, as well as a minimal example with code for loading the
/// configuration below.
///
/// ## TOML (`config.toml`)
/// ```toml
/// nickname = "test"
/// server = "irc.example.com"
/// ```
///
/// ## Rust
/// ```no_run
/// # extern crate irc;
/// use irc::client::prelude::Config;
///
/// # fn main() {
/// let config = Config::load("config.toml").unwrap();
/// # }
/// ```
#[derive(Clone, Deserialize, Serialize, Default, PartialEq, Debug)] #[derive(Clone, Deserialize, Serialize, Default, PartialEq, Debug)]
pub struct Config { pub struct Config {
/// A list of the owners of the client by nickname (for bots). /// A list of the owners of the client by nickname (for bots).
@ -44,6 +88,10 @@ pub struct Config {
pub use_ssl: Option<bool>, pub use_ssl: Option<bool>,
/// The path to the SSL certificate for this server in DER format. /// The path to the SSL certificate for this server in DER format.
pub cert_path: Option<String>, pub cert_path: Option<String>,
/// The path to a SSL certificate to use for CertFP client authentication in DER format.
pub client_cert_path: Option<String>,
/// The password for the certificate to use in CertFP authentication.
pub client_cert_pass: Option<String>,
/// The encoding type used for this connection. /// The encoding type used for this connection.
/// This is typically UTF-8, but could be something else. /// This is typically UTF-8, but could be something else.
pub encoding: Option<String>, pub encoding: Option<String>,
@ -93,6 +141,8 @@ pub struct Config {
/// The path that this configuration was loaded from. /// The path that this configuration was loaded from.
/// ///
/// This should not be specified in any configuration. It will automatically be handled by the library. /// This should not be specified in any configuration. It will automatically be handled by the library.
#[serde(skip_serializing)]
#[doc(hidden)]
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
} }
@ -373,6 +423,16 @@ impl Config {
self.cert_path.as_ref().map(|s| &s[..]) self.cert_path.as_ref().map(|s| &s[..])
} }
/// Gets the path to the client authentication certificate in DER format if specified.
pub fn client_cert_path(&self) -> Option<&str> {
self.client_cert_path.as_ref().map(|s| &s[..])
}
/// Gets the password to the client authentication certificate.
pub fn client_cert_pass(&self) -> &str {
self.client_cert_pass.as_ref().map_or("", |s| &s[..])
}
/// Gets the encoding to use for this connection. This requires the encode feature to work. /// Gets the encoding to use for this connection. This requires the encode feature to work.
/// This defaults to UTF-8 when not specified. /// This defaults to UTF-8 when not specified.
pub fn encoding(&self) -> &str { pub fn encoding(&self) -> &str {
@ -468,13 +528,10 @@ impl Config {
} }
/// Looks up the specified string in the options map. /// Looks up the specified string in the options map.
/// This uses indexing, and thus panics when the string is not present. pub fn get_option(&self, option: &str) -> Option<&str> {
/// This will also panic if used and there are no options. self.options.as_ref().and_then(|o| {
pub fn get_option(&self, option: &str) -> &str { o.get(&option.to_owned()).map(|s| &s[..])
self.options })
.as_ref()
.map(|o| &o[&option.to_owned()][..])
.unwrap()
} }
/// Gets whether or not to use a mock connection for testing. /// Gets whether or not to use a mock connection for testing.
@ -514,6 +571,8 @@ mod test {
port: Some(6667), port: Some(6667),
use_ssl: Some(false), use_ssl: Some(false),
cert_path: None, cert_path: None,
client_cert_path: None,
client_cert_pass: None,
encoding: Some(format!("UTF-8")), encoding: Some(format!("UTF-8")),
channels: Some(vec![format!("#test"), format!("#test2")]), channels: Some(vec![format!("#test"), format!("#test2")]),
channel_keys: None, channel_keys: None,
@ -573,6 +632,7 @@ mod test {
}, },
..Default::default() ..Default::default()
}; };
assert_eq!(cfg.get_option("testing"), "test"); assert_eq!(cfg.get_option("testing"), Some("test"));
assert_eq!(cfg.get_option("not"), None);
} }
} }

View file

@ -36,7 +36,7 @@
//! server.identify().unwrap(); //! server.identify().unwrap();
//! # } //! # }
//! ``` //! ```
use std::borrow::ToOwned; use std::string::ToString;
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
use chrono::prelude::*; use chrono::prelude::*;
@ -105,11 +105,11 @@ pub trait ClientExt: Client {
} }
/// Sends a SASL AUTHENTICATE message with the specified data. /// Sends a SASL AUTHENTICATE message with the specified data.
fn send_sasl(&self, data: &str) -> Result<()> fn send_sasl<S: ToString>(&self, data: S) -> Result<()>
where where
Self: Sized, Self: Sized,
{ {
self.send(AUTHENTICATE(data.to_owned())) self.send(AUTHENTICATE(data.to_string()))
} }
/// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism. /// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism.
@ -138,189 +138,227 @@ pub trait ClientExt: Client {
} }
/// Sends a PONG with the specified message. /// Sends a PONG with the specified message.
fn send_pong(&self, msg: &str) -> Result<()> fn send_pong<S>(&self, msg: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
self.send(PONG(msg.to_owned(), None)) self.send(PONG(msg.to_string(), None))
} }
/// Joins the specified channel or chanlist. /// Joins the specified channel or chanlist.
fn send_join(&self, chanlist: &str) -> Result<()> fn send_join<S>(&self, chanlist: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
self.send(JOIN(chanlist.to_owned(), None, None)) self.send(JOIN(chanlist.to_string(), None, None))
} }
/// Joins the specified channel or chanlist using the specified key or keylist. /// Joins the specified channel or chanlist using the specified key or keylist.
fn send_join_with_keys(&self, chanlist: &str, keylist: &str) -> Result<()> fn send_join_with_keys<S1, S2>(&self, chanlist: &str, keylist: &str) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
self.send(JOIN(chanlist.to_owned(), Some(keylist.to_owned()), None)) self.send(JOIN(chanlist.to_string(), Some(keylist.to_string()), None))
} }
/// Parts the specified channel or chanlist. /// Parts the specified channel or chanlist.
fn send_part(&self, chanlist: &str) -> Result<()> fn send_part<S>(&self, chanlist: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
self.send(PART(chanlist.to_owned(), None)) self.send(PART(chanlist.to_string(), None))
} }
/// Attempts to oper up using the specified username and password. /// Attempts to oper up using the specified username and password.
fn send_oper(&self, username: &str, password: &str) -> Result<()> fn send_oper<S1, S2>(&self, username: S1, password: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
self.send(OPER(username.to_owned(), password.to_owned())) self.send(OPER(username.to_string(), password.to_string()))
} }
/// Sends a message to the specified target. /// Sends a message to the specified target.
fn send_privmsg(&self, target: &str, message: &str) -> Result<()> fn send_privmsg<S1, S2>(&self, target: S1, message: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
let message = message.to_string();
for line in message.split("\r\n") { for line in message.split("\r\n") {
self.send(PRIVMSG(target.to_owned(), line.to_owned()))? self.send(PRIVMSG(target.to_string(), line.to_string()))?
} }
Ok(()) Ok(())
} }
/// Sends a notice to the specified target. /// Sends a notice to the specified target.
fn send_notice(&self, target: &str, message: &str) -> Result<()> fn send_notice<S1, S2>(&self, target: S1, message: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
let message = message.to_string();
for line in message.split("\r\n") { for line in message.split("\r\n") {
self.send(NOTICE(target.to_owned(), line.to_owned()))? self.send(NOTICE(target.to_string(), line.to_string()))?
} }
Ok(()) Ok(())
} }
/// Sets the topic of a channel or requests the current one. /// Sets the topic of a channel or requests the current one.
/// If `topic` is an empty string, it won't be included in the message. /// If `topic` is an empty string, it won't be included in the message.
fn send_topic(&self, channel: &str, topic: &str) -> Result<()> fn send_topic<S1, S2>(&self, channel: S1, topic: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
let topic = topic.to_string();
self.send(TOPIC( self.send(TOPIC(
channel.to_owned(), channel.to_string(),
if topic.is_empty() { if topic.is_empty() {
None None
} else { } else {
Some(topic.to_owned()) Some(topic)
}, },
)) ))
} }
/// Kills the target with the provided message. /// Kills the target with the provided message.
fn send_kill(&self, target: &str, message: &str) -> Result<()> fn send_kill<S1, S2>(&self, target: S1, message: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
self.send(KILL(target.to_owned(), message.to_owned())) self.send(KILL(target.to_string(), message.to_string()))
} }
/// Kicks the listed nicknames from the listed channels with a comment. /// Kicks the listed nicknames from the listed channels with a comment.
/// If `message` is an empty string, it won't be included in the message. /// If `message` is an empty string, it won't be included in the message.
fn send_kick(&self, chanlist: &str, nicklist: &str, message: &str) -> Result<()> fn send_kick<S1, S2, S3>(&self, chanlist: S1, nicklist: S2, message: S3) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
S3: ToString,
{ {
let message = message.to_string();
self.send(KICK( self.send(KICK(
chanlist.to_owned(), chanlist.to_string(),
nicklist.to_owned(), nicklist.to_string(),
if message.is_empty() { if message.is_empty() {
None None
} else { } else {
Some(message.to_owned()) Some(message)
}, },
)) ))
} }
/// Changes the modes for the specified target. /// Changes the modes for the specified target.
fn send_mode<T>(&self, target: &str, modes: &[Mode<T>]) -> Result<()> fn send_mode<S, T>(&self, target: S, modes: &[Mode<T>]) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
T: ModeType, T: ModeType,
{ {
self.send(T::mode(target, modes)) self.send(T::mode(&target.to_string(), modes))
} }
/// Changes the mode of the target by force. /// Changes the mode of the target by force.
/// If `modeparams` is an empty string, it won't be included in the message. /// If `modeparams` is an empty string, it won't be included in the message.
fn send_samode(&self, target: &str, mode: &str, modeparams: &str) -> Result<()> fn send_samode<S1, S2, S3>(&self, target: S1, mode: S2, modeparams: S3) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
S3: ToString,
{ {
let modeparams = modeparams.to_string();
self.send(SAMODE( self.send(SAMODE(
target.to_owned(), target.to_string(),
mode.to_owned(), mode.to_string(),
if modeparams.is_empty() { if modeparams.is_empty() {
None None
} else { } else {
Some(modeparams.to_owned()) Some(modeparams)
}, },
)) ))
} }
/// Forces a user to change from the old nickname to the new nickname. /// Forces a user to change from the old nickname to the new nickname.
fn send_sanick(&self, old_nick: &str, new_nick: &str) -> Result<()> fn send_sanick<S1, S2>(&self, old_nick: S1, new_nick: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
self.send(SANICK(old_nick.to_owned(), new_nick.to_owned())) self.send(SANICK(old_nick.to_string(), new_nick.to_string()))
} }
/// Invites a user to the specified channel. /// Invites a user to the specified channel.
fn send_invite(&self, nick: &str, chan: &str) -> Result<()> fn send_invite<S1, S2>(&self, nick: S1, chan: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
self.send(INVITE(nick.to_owned(), chan.to_owned())) self.send(INVITE(nick.to_string(), chan.to_string()))
} }
/// Quits the server entirely with a message. /// Quits the server entirely with a message.
/// This defaults to `Powered by Rust.` if none is specified. /// This defaults to `Powered by Rust.` if none is specified.
fn send_quit(&self, msg: &str) -> Result<()> fn send_quit<S>(&self, msg: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
let msg = msg.to_string();
self.send(QUIT(Some(if msg.is_empty() { self.send(QUIT(Some(if msg.is_empty() {
"Powered by Rust.".to_owned() "Powered by Rust.".to_string()
} else { } else {
msg.to_owned() msg
}))) })))
} }
/// Sends a CTCP-escaped message to the specified target. /// Sends a CTCP-escaped message to the specified target.
/// This requires the CTCP feature to be enabled. /// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_ctcp(&self, target: &str, msg: &str) -> Result<()> fn send_ctcp<S1, S2>(&self, target: S1, msg: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
self.send_privmsg(target, &format!("\u{001}{}\u{001}", msg)[..]) self.send_privmsg(target, &format!("\u{001}{}\u{001}", msg.to_string())[..])
} }
/// Sends an action command to the specified target. /// Sends an action command to the specified target.
/// This requires the CTCP feature to be enabled. /// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_action(&self, target: &str, msg: &str) -> Result<()> fn send_action<S1, S2>(&self, target: S1, msg: S2) -> Result<()>
where where
Self: Sized, Self: Sized,
S1: ToString,
S2: ToString,
{ {
self.send_ctcp(target, &format!("ACTION {}", msg)[..]) self.send_ctcp(target, &format!("ACTION {}", msg.to_string())[..])
} }
/// Sends a finger request to the specified target. /// Sends a finger request to the specified target.
/// This requires the CTCP feature to be enabled. /// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_finger(&self, target: &str) -> Result<()> fn send_finger<S: ToString>(&self, target: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
self.send_ctcp(target, "FINGER") self.send_ctcp(target, "FINGER")
} }
@ -328,9 +366,10 @@ pub trait ClientExt: Client {
/// Sends a version request to the specified target. /// Sends a version request to the specified target.
/// This requires the CTCP feature to be enabled. /// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_version(&self, target: &str) -> Result<()> fn send_version<S>(&self, target: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
self.send_ctcp(target, "VERSION") self.send_ctcp(target, "VERSION")
} }
@ -338,9 +377,10 @@ pub trait ClientExt: Client {
/// Sends a source request to the specified target. /// Sends a source request to the specified target.
/// This requires the CTCP feature to be enabled. /// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_source(&self, target: &str) -> Result<()> fn send_source<S>(&self, target: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
self.send_ctcp(target, "SOURCE") self.send_ctcp(target, "SOURCE")
} }
@ -348,9 +388,10 @@ pub trait ClientExt: Client {
/// Sends a user info request to the specified target. /// Sends a user info request to the specified target.
/// This requires the CTCP feature to be enabled. /// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_user_info(&self, target: &str) -> Result<()> fn send_user_info<S>(&self, target: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
self.send_ctcp(target, "USERINFO") self.send_ctcp(target, "USERINFO")
} }
@ -358,9 +399,10 @@ pub trait ClientExt: Client {
/// Sends a finger request to the specified target. /// Sends a finger request to the specified target.
/// This requires the CTCP feature to be enabled. /// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_ctcp_ping(&self, target: &str) -> Result<()> fn send_ctcp_ping<S>(&self, target: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
let time = Local::now(); let time = Local::now();
self.send_ctcp(target, &format!("PING {}", time.timestamp())[..]) self.send_ctcp(target, &format!("PING {}", time.timestamp())[..])
@ -369,9 +411,10 @@ pub trait ClientExt: Client {
/// Sends a time request to the specified target. /// Sends a time request to the specified target.
/// This requires the CTCP feature to be enabled. /// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")] #[cfg(feature = "ctcp")]
fn send_time(&self, target: &str) -> Result<()> fn send_time<S>(&self, target: S) -> Result<()>
where where
Self: Sized, Self: Sized,
S: ToString,
{ {
self.send_ctcp(target, "TIME") self.send_ctcp(target, "TIME")
} }

View file

@ -45,8 +45,6 @@
//! # } //! # }
//! ``` //! ```
#[cfg(feature = "ctcp")]
use std::ascii::AsciiExt;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
@ -363,7 +361,7 @@ impl ClientState {
let config_chans = self.config().channels(); let config_chans = self.config().channels();
for chan in &config_chans { for chan in &config_chans {
match self.config().channel_key(chan) { match self.config().channel_key(chan) {
Some(key) => self.send_join_with_keys(chan, key)?, Some(key) => self.send_join_with_keys::<&str, &str>(chan, key)?,
None => self.send_join(chan)?, None => self.send_join(chan)?,
} }
} }
@ -444,7 +442,7 @@ impl ClientState {
} }
#[cfg(feature = "nochanlists")] #[cfg(feature = "nochanlists")]
fn handle_part(&self, src: &str, chan: &str) {} fn handle_part(&self, _: &str, _: &str) {}
#[cfg(not(feature = "nochanlists"))] #[cfg(not(feature = "nochanlists"))]
fn handle_part(&self, src: &str, chan: &str) { fn handle_part(&self, src: &str, chan: &str) {
@ -495,7 +493,7 @@ impl ClientState {
} }
#[cfg(feature = "nochanlists")] #[cfg(feature = "nochanlists")]
fn handle_mode(&self, _: &str, _: &[Mode<ChannelMODE>]) {} fn handle_mode(&self, _: &str, _: &[Mode<ChannelMode>]) {}
#[cfg(not(feature = "nochanlists"))] #[cfg(not(feature = "nochanlists"))]
fn handle_mode(&self, chan: &str, modes: &[Mode<ChannelMode>]) { fn handle_mode(&self, chan: &str, modes: &[Mode<ChannelMode>]) {

View file

@ -138,8 +138,8 @@ impl IrcReactor {
/// # } /// # }
/// ``` /// ```
pub fn register_client_with_handler<F, U>( pub fn register_client_with_handler<F, U>(
&mut self, client: IrcClient, handler: F &mut self, client: IrcClient, mut handler: F
) where F: Fn(&IrcClient, Message) -> U + 'static, ) where F: FnMut(&IrcClient, Message) -> U + 'static,
U: IntoFuture<Item = (), Error = error::IrcError> + 'static { U: IntoFuture<Item = (), Error = error::IrcError> + 'static {
self.handlers.push(Box::new(client.stream().for_each(move |message| { self.handlers.push(Box::new(client.stream().for_each(move |message| {
handler(&client, message) handler(&client, message)

View file

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

View file

@ -3,6 +3,7 @@
use std::io::Error as IoError; use std::io::Error as IoError;
use std::sync::mpsc::RecvError; use std::sync::mpsc::RecvError;
use failure;
use futures::sync::mpsc::SendError; use futures::sync::mpsc::SendError;
use futures::sync::oneshot::Canceled; use futures::sync::oneshot::Canceled;
use native_tls::Error as TlsError; use native_tls::Error as TlsError;
@ -94,7 +95,16 @@ pub enum IrcError {
/// All specified nicknames were in use or unusable. /// All specified nicknames were in use or unusable.
#[fail(display = "none of the specified nicknames were usable")] #[fail(display = "none of the specified nicknames were usable")]
NoUsableNick NoUsableNick,
/// This allows you to produce any `failure::Error` within closures used by
/// the irc crate. No errors of this kind will ever be produced by the crate
/// itself.
#[fail(display = "{}", inner)]
Custom {
/// The actual error that occurred.
inner: failure::Error
},
} }
/// Errors that occur when parsing messages. /// Errors that occur when parsing messages.

View file

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

188
src/proto/colors.rs Normal file
View file

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

View file

@ -1,5 +1,4 @@
//! Enumeration of all available client commands. //! Enumeration of all available client commands.
use std::ascii::AsciiExt;
use std::str::FromStr; use std::str::FromStr;
use error::MessageParseError; use error::MessageParseError;
@ -49,8 +48,32 @@ pub enum Command {
// 3.3 Sending messages // 3.3 Sending messages
/// PRIVMSG msgtarget :message /// PRIVMSG msgtarget :message
///
/// ## Responding to a `PRIVMSG`
///
/// When responding to a message, it is not sufficient to simply copy the message target
/// (msgtarget). This will work just fine for responding to messages in channels where the
/// target is the same for all participants. However, when the message is sent directly to a
/// user, this target will be that client's username, and responding to that same target will
/// actually mean sending itself a response. In such a case, you should instead respond to the
/// user sending the message as specified in the message prefix. Since this is a common
/// pattern, there is a utility function
/// [`Message::response_target`](../message/struct.Message.html#method.response_target)
/// which is used for this exact purpose.
PRIVMSG(String, String), PRIVMSG(String, String),
/// NOTICE msgtarget :message /// NOTICE msgtarget :message
///
/// ## Responding to a `NOTICE`
///
/// When responding to a notice, it is not sufficient to simply copy the message target
/// (msgtarget). This will work just fine for responding to messages in channels where the
/// target is the same for all participants. However, when the message is sent directly to a
/// user, this target will be that client's username, and responding to that same target will
/// actually mean sending itself a response. In such a case, you should instead respond to the
/// user sending the message as specified in the message prefix. Since this is a common
/// pattern, there is a utility function
/// [`Message::response_target`](../message/struct.Message.html#method.response_target)
/// which is used for this exact purpose.
NOTICE(String, String), NOTICE(String, String),
// 3.4 Server queries and commands // 3.4 Server queries and commands
@ -421,18 +444,14 @@ impl<'a> From<&'a Command> for String {
Command::CHGHOST(ref u, ref h) => stringify("CHGHOST", &[u, h], None), Command::CHGHOST(ref u, ref h) => stringify("CHGHOST", &[u, h], None),
Command::Response(ref resp, ref a, Some(ref s)) => { Command::Response(ref resp, ref a, Some(ref s)) => {
stringify( stringify(&format!("{:03}", *resp as u16),
&format!("{}", *resp as u16),
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(), &a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
Some(s), Some(s))
)
} }
Command::Response(ref resp, ref a, None) => { Command::Response(ref resp, ref a, None) => {
stringify( stringify(&format!("{:03}", *resp as u16),
&format!("{}", *resp as u16),
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(), &a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
None, None)
)
} }
Command::Raw(ref c, ref a, Some(ref s)) => { Command::Raw(ref c, ref a, Some(ref s)) => {
stringify(c, &a.iter().map(|s| &s[..]).collect::<Vec<_>>(), Some(s)) stringify(c, &a.iter().map(|s| &s[..]).collect::<Vec<_>>(), Some(s))
@ -1763,3 +1782,16 @@ impl FromStr for BatchSubCommand {
} }
} }
} }
#[cfg(test)]
mod test {
use super::Response;
use super::Command;
#[test]
fn format_response() {
assert!(String::from(&Command::Response(Response::RPL_WELCOME,
vec!["foo".into()],
None)) == "001 foo");
}
}

View file

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

View file

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

View file

@ -3,6 +3,7 @@
pub mod caps; pub mod caps;
pub mod chan; pub mod chan;
pub mod command; pub mod command;
pub mod colors;
pub mod irc; pub mod irc;
pub mod line; pub mod line;
pub mod message; pub mod message;
@ -11,6 +12,7 @@ pub mod response;
pub use self::caps::{Capability, NegotiationVersion}; pub use self::caps::{Capability, NegotiationVersion};
pub use self::chan::ChannelExt; pub use self::chan::ChannelExt;
pub use self::colors::FormattedStringExt;
pub use self::command::{BatchSubCommand, CapSubCommand, Command}; pub use self::command::{BatchSubCommand, CapSubCommand, Command};
pub use self::irc::IrcCodec; pub use self::irc::IrcCodec;
pub use self::message::Message; pub use self::message::Message;