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
irc:
channels:
- "irc.fyrechat.net#vana-commits"
- "ircs://irc.pdgn.co:6697/#commits"
template:
- "%{repository_slug}/%{branch} (%{commit} - %{author}): %{message}"
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).
* 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
* Create a topic branch from where you want to base your work.

View file

@ -1,10 +1,10 @@
[package]
name = "irc"
version = "0.13.5"
description = "A simple, thread-safe, and async-friendly library for IRC clients."
version = "0.13.6"
description = "the irc crate usable, async IRC for Rust "
authors = ["Aaron Weiss <awe@pdgn.co>"]
license = "MPL-2.0"
keywords = ["irc", "client", "thread-safe", "async", "tokio"]
keywords = ["irc", "client", "thread-safe", "async", "tokio", "protocol"]
categories = ["asynchronous", "network-programming"]
documentation = "https://docs.rs/irc/"
repository = "https://github.com/aatxe/irc"
@ -12,6 +12,8 @@ readme = "README.md"
[badges]
travis-ci = { repository = "aatxe/irc" }
is-it-maintained-issue-resolution = { repository = "aatxe/irc" }
is-it-maintained-open-issues = { repository = "aatxe/irc" }
[features]
default = ["ctcp", "toml"]
@ -27,17 +29,18 @@ chrono = "0.4"
encoding = "0.2"
failure = "0.1"
futures = "0.1"
log = "0.3"
native-tls = "0.1"
log = "0.4"
native-tls = "0.2"
serde = "1.0"
serde_derive = "1.0"
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.7", optional = true }
tokio-codec = "0.1"
tokio-core = "0.1"
tokio-io = "0.1"
tokio-mockstream = "1.1"
tokio-timer = "0.1"
tokio-tls = "0.1"
tokio-tls = "0.2"
toml = { version = "0.4", optional = true }
[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]: https://travis-ci.org/aatxe/irc
[cr-badge]: https://img.shields.io/crates/v/irc.svg
[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]: 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
[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
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
projects:
@ -26,26 +25,32 @@ projects:
- [alectro][alectro] — a terminal IRC client
- [spilo][spilo] — a minimalistic IRC bouncer
- [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]
- [url-bot-rs][url-bot-rs] — a URL-fetching IRC bot
[alectro]: https://github.com/aatxe/alectro
[spilo]: https://github.com/aatxe/spilo
[ircbot]: https://github.com/8573/irc-bot.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
[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!
## Getting Started ##
## Getting Started
To start using the irc crate with cargo, you can simply 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
[doc root][doc]. You'll find a number of examples in `examples/`, throughout the documentation, and
below.
To start using the irc crate with cargo, you can add `irc = "0.13"` to your dependencies in
your Cargo.toml file. The high-level API can be found in [`irc::client::prelude`][irc-prelude].
You'll find a number of examples to help you get started in `examples/`, throughout the
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
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
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
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
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
explanations of the various fields on
[docs.rs](https://docs.rs/irc/0.13.2/irc/client/data/config/struct.Config.html#fields).
explanations of the various fields on [docs.rs][config-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:
@ -139,6 +145,8 @@ port = 6697
password = ""
use_ssl = true
cert_path = "cert.der"
client_cert_path = "client.der"
client_cert_pass = "password"
encoding = "UTF-8"
channels = ["#rust", "#haskell", "#fake"]
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
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,
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

View file

@ -7,15 +7,15 @@ 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()]),
server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#rust-spam".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()]),
server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default()
};

View file

@ -8,8 +8,8 @@ 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()]),
server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default()
};

View file

@ -7,15 +7,15 @@ 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()]),
server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#rust-spam".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()]),
server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default()
};

View file

@ -7,8 +7,8 @@ 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()]),
server: Some("irc.mozilla.org".to_owned()),
channels: Some(vec!["#rust-spam".to_owned()]),
..Default::default()
};

View file

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

View file

@ -6,12 +6,12 @@ use std::io::Read;
use encoding::EncoderTrap;
use encoding::label::encoding_from_whatwg_label;
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::net::{TcpStream, TcpStreamNew};
use tokio_io::AsyncRead;
use tokio_mockstream::MockStream;
use tokio_tls::{TlsConnectorExt, TlsStream};
use tokio_tls::{self, TlsStream};
use error;
use client::data::Config;
@ -81,13 +81,15 @@ impl<'a> Future for ConnectionFuture<'a> {
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
match *self {
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);
Ok(Async::Ready(Connection::Unsecured(transport)))
}
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);
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);
Ok(Async::Ready(Connection::Mock(Logged::wrap(transport))))
@ -126,21 +129,30 @@ impl Connection {
} else if config.use_ssl() {
let domain = format!("{}", config.server()?);
info!("Connecting via SSL to {}.", domain);
let mut builder = TlsConnector::builder()?;
let mut builder = TlsConnector::builder();
if let Some(cert_path) = config.cert_path() {
let mut file = File::open(cert_path)?;
let mut cert_data = vec![];
file.read_to_end(&mut 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);
}
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 res: error::IrcError = e.into();
res
}).and_then(move |socket| {
connector.connect_async(&domain, socket).map_err(
connector.connect(&domain, socket).map_err(
|e| e.into(),
)
}));

View file

@ -18,7 +18,51 @@ use error::TomlError;
use error::{ConfigError, Result};
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)]
pub struct Config {
/// A list of the owners of the client by nickname (for bots).
@ -44,6 +88,10 @@ pub struct Config {
pub use_ssl: Option<bool>,
/// The path to the SSL certificate for this server in DER format.
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.
/// This is typically UTF-8, but could be something else.
pub encoding: Option<String>,
@ -93,6 +141,8 @@ pub struct Config {
/// The path that this configuration was loaded from.
///
/// 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>,
}
@ -373,6 +423,16 @@ impl Config {
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.
/// This defaults to UTF-8 when not specified.
pub fn encoding(&self) -> &str {
@ -468,13 +528,10 @@ impl Config {
}
/// Looks up the specified string in the options map.
/// This uses indexing, and thus panics when the string is not present.
/// This will also panic if used and there are no options.
pub fn get_option(&self, option: &str) -> &str {
self.options
.as_ref()
.map(|o| &o[&option.to_owned()][..])
.unwrap()
pub fn get_option(&self, option: &str) -> Option<&str> {
self.options.as_ref().and_then(|o| {
o.get(&option.to_owned()).map(|s| &s[..])
})
}
/// Gets whether or not to use a mock connection for testing.
@ -514,6 +571,8 @@ mod test {
port: Some(6667),
use_ssl: Some(false),
cert_path: None,
client_cert_path: None,
client_cert_pass: None,
encoding: Some(format!("UTF-8")),
channels: Some(vec![format!("#test"), format!("#test2")]),
channel_keys: None,
@ -573,6 +632,7 @@ mod test {
},
..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();
//! # }
//! ```
use std::borrow::ToOwned;
use std::string::ToString;
#[cfg(feature = "ctcp")]
use chrono::prelude::*;
@ -105,11 +105,11 @@ pub trait ClientExt: Client {
}
/// 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
Self: Sized,
{
self.send(AUTHENTICATE(data.to_owned()))
self.send(AUTHENTICATE(data.to_string()))
}
/// 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.
fn send_pong(&self, msg: &str) -> Result<()>
fn send_pong<S>(&self, msg: S) -> Result<()>
where
Self: Sized,
S: ToString,
{
self.send(PONG(msg.to_owned(), None))
self.send(PONG(msg.to_string(), None))
}
/// Joins the specified channel or chanlist.
fn send_join(&self, chanlist: &str) -> Result<()>
fn send_join<S>(&self, chanlist: S) -> Result<()>
where
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.
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
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.
fn send_part(&self, chanlist: &str) -> Result<()>
fn send_part<S>(&self, chanlist: S) -> Result<()>
where
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.
fn send_oper(&self, username: &str, password: &str) -> Result<()>
fn send_oper<S1, S2>(&self, username: S1, password: S2) -> Result<()>
where
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.
fn send_privmsg(&self, target: &str, message: &str) -> Result<()>
fn send_privmsg<S1, S2>(&self, target: S1, message: S2) -> Result<()>
where
Self: Sized,
S1: ToString,
S2: ToString,
{
let message = message.to_string();
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(())
}
/// 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
Self: Sized,
S1: ToString,
S2: ToString,
{
let message = message.to_string();
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(())
}
/// 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.
fn send_topic(&self, channel: &str, topic: &str) -> Result<()>
fn send_topic<S1, S2>(&self, channel: S1, topic: S2) -> Result<()>
where
Self: Sized,
S1: ToString,
S2: ToString,
{
let topic = topic.to_string();
self.send(TOPIC(
channel.to_owned(),
channel.to_string(),
if topic.is_empty() {
None
} else {
Some(topic.to_owned())
Some(topic)
},
))
}
/// 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
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.
/// 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
Self: Sized,
S1: ToString,
S2: ToString,
S3: ToString,
{
let message = message.to_string();
self.send(KICK(
chanlist.to_owned(),
nicklist.to_owned(),
chanlist.to_string(),
nicklist.to_string(),
if message.is_empty() {
None
} else {
Some(message.to_owned())
Some(message)
},
))
}
/// 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
Self: Sized,
S: ToString,
T: ModeType,
{
self.send(T::mode(target, modes))
self.send(T::mode(&target.to_string(), modes))
}
/// Changes the mode of the target by force.
/// 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
Self: Sized,
S1: ToString,
S2: ToString,
S3: ToString,
{
let modeparams = modeparams.to_string();
self.send(SAMODE(
target.to_owned(),
mode.to_owned(),
target.to_string(),
mode.to_string(),
if modeparams.is_empty() {
None
} else {
Some(modeparams.to_owned())
Some(modeparams)
},
))
}
/// 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
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.
fn send_invite(&self, nick: &str, chan: &str) -> Result<()>
fn send_invite<S1, S2>(&self, nick: S1, chan: S2) -> Result<()>
where
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.
/// 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
Self: Sized,
S: ToString,
{
let msg = msg.to_string();
self.send(QUIT(Some(if msg.is_empty() {
"Powered by Rust.".to_owned()
"Powered by Rust.".to_string()
} else {
msg.to_owned()
msg
})))
}
/// Sends a CTCP-escaped message to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_ctcp(&self, target: &str, msg: &str) -> Result<()>
fn send_ctcp<S1, S2>(&self, target: S1, msg: S2) -> Result<()>
where
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.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_action(&self, target: &str, msg: &str) -> Result<()>
fn send_action<S1, S2>(&self, target: S1, msg: S2) -> Result<()>
where
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.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_finger(&self, target: &str) -> Result<()>
fn send_finger<S: ToString>(&self, target: S) -> Result<()>
where
Self: Sized,
S: ToString,
{
self.send_ctcp(target, "FINGER")
}
@ -328,9 +366,10 @@ pub trait ClientExt: Client {
/// Sends a version request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_version(&self, target: &str) -> Result<()>
fn send_version<S>(&self, target: S) -> Result<()>
where
Self: Sized,
S: ToString,
{
self.send_ctcp(target, "VERSION")
}
@ -338,9 +377,10 @@ pub trait ClientExt: Client {
/// Sends a source request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_source(&self, target: &str) -> Result<()>
fn send_source<S>(&self, target: S) -> Result<()>
where
Self: Sized,
S: ToString,
{
self.send_ctcp(target, "SOURCE")
}
@ -348,9 +388,10 @@ pub trait ClientExt: Client {
/// Sends a user info request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_user_info(&self, target: &str) -> Result<()>
fn send_user_info<S>(&self, target: S) -> Result<()>
where
Self: Sized,
S: ToString,
{
self.send_ctcp(target, "USERINFO")
}
@ -358,9 +399,10 @@ pub trait ClientExt: Client {
/// Sends a finger request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_ctcp_ping(&self, target: &str) -> Result<()>
fn send_ctcp_ping<S>(&self, target: S) -> Result<()>
where
Self: Sized,
S: ToString,
{
let time = Local::now();
self.send_ctcp(target, &format!("PING {}", time.timestamp())[..])
@ -369,9 +411,10 @@ pub trait ClientExt: Client {
/// Sends a time request to the specified target.
/// This requires the CTCP feature to be enabled.
#[cfg(feature = "ctcp")]
fn send_time(&self, target: &str) -> Result<()>
fn send_time<S>(&self, target: S) -> Result<()>
where
Self: Sized,
S: ToString,
{
self.send_ctcp(target, "TIME")
}

View file

@ -45,8 +45,6 @@
//! # }
//! ```
#[cfg(feature = "ctcp")]
use std::ascii::AsciiExt;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex, RwLock};
@ -363,7 +361,7 @@ impl ClientState {
let config_chans = self.config().channels();
for chan in &config_chans {
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)?,
}
}
@ -444,7 +442,7 @@ impl ClientState {
}
#[cfg(feature = "nochanlists")]
fn handle_part(&self, src: &str, chan: &str) {}
fn handle_part(&self, _: &str, _: &str) {}
#[cfg(not(feature = "nochanlists"))]
fn handle_part(&self, src: &str, chan: &str) {
@ -495,7 +493,7 @@ impl ClientState {
}
#[cfg(feature = "nochanlists")]
fn handle_mode(&self, _: &str, _: &[Mode<ChannelMODE>]) {}
fn handle_mode(&self, _: &str, _: &[Mode<ChannelMode>]) {}
#[cfg(not(feature = "nochanlists"))]
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>(
&mut self, client: IrcClient, handler: F
) where F: Fn(&IrcClient, Message) -> U + 'static,
&mut self, client: IrcClient, mut handler: F
) where F: FnMut(&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)

View file

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

View file

@ -3,6 +3,7 @@
use std::io::Error as IoError;
use std::sync::mpsc::RecvError;
use failure;
use futures::sync::mpsc::SendError;
use futures::sync::oneshot::Canceled;
use native_tls::Error as TlsError;
@ -94,7 +95,16 @@ pub enum IrcError {
/// All specified nicknames were in use or unusable.
#[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.

View file

@ -58,6 +58,7 @@ extern crate serde_derive;
extern crate serde_json;
#[cfg(feature = "yaml")]
extern crate serde_yaml;
extern crate tokio_codec;
extern crate tokio_core;
extern crate tokio_io;
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.
use std::ascii::AsciiExt;
use std::str::FromStr;
use error::MessageParseError;
@ -49,8 +48,32 @@ pub enum Command {
// 3.3 Sending messages
/// 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),
/// 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),
// 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::Response(ref resp, ref a, Some(ref s)) => {
stringify(
&format!("{}", *resp as u16),
stringify(&format!("{:03}", *resp as u16),
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
Some(s),
)
Some(s))
}
Command::Response(ref resp, ref a, None) => {
stringify(
&format!("{}", *resp as u16),
stringify(&format!("{:03}", *resp as u16),
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
None,
)
None)
}
Command::Raw(ref c, ref a, Some(ref 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.
use bytes::BytesMut;
use tokio_io::codec::{Decoder, Encoder};
use tokio_codec::{Decoder, Encoder};
use error;
use proto::line::LineCodec;

View file

@ -12,13 +12,14 @@ use error;
/// A line-based codec parameterized by an encoding.
pub struct LineCodec {
encoding: EncodingRef,
next_index: usize,
}
impl LineCodec {
/// Creates a new instance of LineCodec from the specified encoding.
pub fn new(label: &str) -> error::Result<LineCodec> {
encoding_from_whatwg_label(label)
.map(|enc| LineCodec { encoding: enc })
.map(|enc| LineCodec { encoding: enc, next_index: 0 })
.ok_or_else(|| io::Error::new(
io::ErrorKind::InvalidInput,
&format!("Attempted to use unknown codec {}.", label)[..],
@ -31,9 +32,12 @@ impl Decoder for LineCodec {
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') {
if let Some(offset) = src[self.next_index..].iter().position(|b| *b == b'\n') {
// 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.
match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) {
@ -46,6 +50,9 @@ impl Decoder for LineCodec {
),
}
} 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)
}
}

View file

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