commit
0f6f72d4c7
24 changed files with 560 additions and 136 deletions
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
15
Cargo.toml
15
Cargo.toml
|
@ -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]
|
||||
|
|
42
README.md
42
README.md
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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
51
examples/tooter.rs
Normal 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();
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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>]) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
12
src/error.rs
12
src/error.rs
|
@ -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.
|
||||
|
|
|
@ -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
188
src/proto/colors.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue