This commit is contained in:
.[d]. 2021-02-17 15:30:48 -06:00
джерело 2e0bf31c2d
коміт 6f1fdf0ec6
43 змінених файлів з 8101 додано та 0 видалено

1
.gitignore сторонній Normal file

@ -0,0 +1 @@
/target

17
Cargo.toml Normal file

@ -0,0 +1,17 @@
[package]
name = "decoded roobot"
version = "0.1.0"
authors = [".[d]. <d@aol.com>"]
edition = "2018"
[workspace]
members = ["irc"]
[[bin]]
name = "decoded"
path = "src/main.rs"
[dependencies]
futures = { version = "0.3.*" }
tokio = {version = "0.2.*", features = ["full"] }
irc = { version = "0.14.0", path = "irc" }

2
build.sh Executable file

@ -0,0 +1,2 @@
cargo build
./target/debug/decoded

90
irc/Cargo.toml Normal file

@ -0,0 +1,90 @@
[package]
name = "irc"
version = "0.14.0"
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"]
categories = ["asynchronous", "network-programming"]
documentation = "https://docs.rs/irc/"
repository = "https://github.com/aatxe/irc"
readme = "README.md"
edition = "2018"
[badges]
travis-ci = { repository = "aatxe/irc" }
is-it-maintained-issue-resolution = { repository = "aatxe/irc" }
is-it-maintained-open-issues = { repository = "aatxe/irc" }
#[workspace]
#members = [ "irc-proto/" ]
[features]
default = ["ctcp", "tls-native", "toml_config"]
ctcp = []
nochanlists = []
json_config = ["serde", "serde/derive", "serde_derive", "serde_json"]
toml_config = ["serde", "serde/derive", "serde_derive", "toml"]
yaml_config = ["serde", "serde/derive", "serde_derive", "serde_yaml"]
# Temporary transitionary features
json = ["json_config"]
yaml = ["yaml_config"]
proxy = ["tokio-socks"]
tls-native = ["native-tls", "tokio-tls"]
tls-rust = ["tokio-rustls", "webpki-roots"]
[dependencies]
bufstream = "0.1.0"
bytes = "0.5.0"
chrono = "0.4.0"
encoding = "0.2.0"
futures-channel = "0.3.0"
futures-util = { version = "0.3.0", features = ["sink"] }
irc-proto = { version = "0.14.0", path = "irc-proto" }
log = "0.4.0"
parking_lot = "0.10.0"
pin-utils = "0.1.0-alpha.4"
thiserror = "1.0.0"
tokio = { version = "0.2.0", features = ["macros", "net", "stream", "time"] }
tokio-util = { version = "0.3.0", features = ["codec"] }
# Feature - Config
serde = { version = "1.0.0", optional = true }
serde_derive = { version = "1.0.0", optional = true }
serde_json = { version = "1.0.0", optional = true }
serde_yaml = { version = "0.8.0", optional = true }
toml = { version = "0.5.0", optional = true }
# Feature - Proxy
tokio-socks = { version = "0.2.0", optional = true }
# Feature - TLS
native-tls = { version = "0.2.0", optional = true }
tokio-rustls = { version = "0.13.0", optional = true }
tokio-tls = { version = "0.3.0", optional = true }
webpki-roots = { version = "0.19.0", optional = true }
[dev-dependencies]
anyhow = "1.0.0"
args = "2.0.0"
env_logger = "0.7.0"
futures = "0.3.0"
getopts = "0.2.0"
[[example]]
name = "simple_proxy"
path = "examples/simple_proxy.rs"
required-features = ["proxy"]
[[example]]
name = "simple_plaintext"
path = "examples/simple_plaintext.rs"
required-features = ["tls-native"]

156
irc/README.md Normal file

@ -0,0 +1,156 @@
# 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
[rfc2812]: http://tools.ietf.org/html/rfc2812
[ircv3.1]: http://ircv3.net/irc/3.1.html
[ircv3.2]: http://ircv3.net/irc/3.2.html
"the irc crate" is a thread-safe and async-friendly IRC client library written in Rust. It's
compliant with [RFC 2812][rfc2812], [IRCv3.1][ircv3.1], [IRCv3.2][ircv3.2], and includes some
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
the irc crate is being used to build new IRC software in Rust. Here are some of our favorite
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
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.
[irc-prelude]: https://docs.rs/irc/*/irc/client/prelude/index.html
## Using Futures
The release of v0.14 replaced all existing APIs with one based on async/await.
```rust,no_run,edition2018
use irc::client::prelude::*;
use futures::prelude::*;
#[tokio::main]
async fn main() -> Result<(), failure::Error> {
// We can also load the Config at runtime via Config::load("path/to/config.toml")
let config = Config {
nickname: Some("the-irc-crate".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: Some(vec!["#test".to_owned()]),
..Config::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
while let Some(message) = stream.next().await.transpose()? {
print!("{}", message);
}
Ok(())
}
```
## 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
sufficient for most IRC bots. Programmatic configuration is convenient for writing tests, but can
also be useful when defining your own custom configuration format that can be converted to `Config`.
The primary configuration format is TOML, but if you are so inclined, you can use JSON and/or YAML
via the optional `json_config` and `yaml_config` 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][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:
```toml
owners = []
nickname = "user"
nick_password = "password"
alt_nicks = ["user_", "user__"]
username = "user"
realname = "Test User"
server = "chat.freenode.net"
port = 6697
password = ""
proxy_type = "None"
proxy_server = "127.0.0.1"
proxy_port = "1080"
proxy_username = ""
proxy_password = ""
use_tls = true
cert_path = "cert.der"
client_cert_path = "client.der"
client_cert_pass = "password"
encoding = "UTF-8"
channels = ["#rust", "#haskell", "#fake"]
umodes = "+RB-x"
user_info = "I'm a test user for the irc crate."
version = "irc:git:Rust"
source = "https://github.com/aatxe/irc"
ping_time = 180
ping_timeout = 10
burst_window_length = 8
max_messages_in_burst = 15
should_ghost = false
ghost_sequence = []
[channel_keys]
"#fake" = "password"
[options]
note = "anything you want can be in here!"
and = "you can use it to build your own additional configuration options."
key = "value"
```
You can convert between different configuration formats with `convertconf` like so:
```shell
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 easier for users to migrate their old configurations to TOML.
## 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
`LICENSE.md`. To foster an inclusive community around the irc crate, we have adopted a Code of
Conduct whose text can be found in `CODE_OF_CONDUCT.md`. You can find details about how to
contribute in `CONTRIBUTING.md`.
[awe]: https://github.com/aatxe/
[bun]: https://github.com/retep998/

51
irc/examples/build-bot.rs Normal file

@ -0,0 +1,51 @@
extern crate irc;
use futures::prelude::*;
use irc::client::prelude::*;
use std::default::Default;
use std::env;
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let repository_slug = env::var("TRAVIS_REPO_SLUG").unwrap();
let branch = env::var("TRAVIS_BRANCH").unwrap();
let commit = env::var("TRAVIS_COMMIT").unwrap();
let commit_message = env::var("TRAVIS_COMMIT_MESSAGE").unwrap();
let features = env::var("FEATURES").unwrap();
let config = Config {
nickname: Some("irc-crate-ci".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
alt_nicks: vec!["[irc-crate-ci]".to_owned()],
..Default::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
while let Some(message) = stream.next().await.transpose()? {
match message.command {
Command::Response(Response::RPL_ISUPPORT, _) => {
client.send_privmsg(
"#commits",
format!(
"[{}/{}] ({}) {} [{}]",
repository_slug,
branch,
&commit[..7],
commit_message,
features,
),
)?;
client.send_quit("QUIT")?;
}
_ => (),
}
}
Ok(())
}

@ -0,0 +1,59 @@
use std::env;
use std::process::exit;
use args::{Args, ArgsError};
use getopts::Occur;
use irc::client::data::Config;
const PROGRAM_DESC: &str = "Use this program to convert configs between {JSON, TOML, YAML}.";
const PROGRAM_NAME: &str = "convertconf";
fn main() {
let args: Vec<_> = env::args().collect();
match parse(&args) {
Ok(Some((ref input, ref output))) => {
let mut cfg = Config::load(input).unwrap();
cfg.save(output).unwrap();
println!("Converted {} to {}.", input, output);
}
Ok(None) => {
println!("Failed to provide required arguments.");
exit(1);
}
Err(err) => {
println!("{}", err);
exit(1);
}
}
}
fn parse(input: &[String]) -> Result<Option<(String, String)>, ArgsError> {
let mut args = Args::new(PROGRAM_NAME, PROGRAM_DESC);
args.flag("h", "help", "Print the usage menu");
args.option(
"i",
"input",
"The path to the input config",
"FILE",
Occur::Req,
None,
);
args.option(
"o",
"output",
"The path to output the new config to",
"FILE",
Occur::Req,
None,
);
args.parse(input)?;
let help = args.value_of("help")?;
if help {
args.full_usage();
return Ok(None);
}
Ok(Some((args.value_of("input")?, args.value_of("output")?)))
}

33
irc/examples/decoded.rs Normal file

@ -0,0 +1,33 @@
use futures::prelude::*;
use irc::client::prelude::*;
use std::time::Duration;
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("wumpscut".to_owned()),
server: Some("ircd.chat".to_owned()),
channels: vec!["#tcpdirect".to_owned()],
port: Some(6697),
use_tls: Some(true),
..Default::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
let mut interval = tokio::time::interval(Duration::from_secs(10)).fuse();
loop {
futures::select! {
m = stream.select_next_some() => {
println!("{}", m?);
}
_ = interval.select_next_some() => {
client.send_privmsg("#tcpdirect", ".[d]. ssl+insecure rust bot")?;
}
}
}
}

@ -0,0 +1,58 @@
use futures::prelude::*;
use irc::{client::prelude::*, error};
#[tokio::main]
async fn main() -> irc::error::Result<()> {
env_logger::init();
let cfg1 = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()],
..Default::default()
};
let cfg2 = Config {
nickname: Some("bananas".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()],
..Default::default()
};
let configs = vec![cfg1, cfg2];
let mut streams = Vec::new();
let mut senders = Vec::new();
for config in configs {
// Immediate errors like failure to resolve the server's domain or to establish any connection will
// manifest here in the result of prepare_client_and_connect.
let mut client = Client::from_config(config).await?;
client.identify()?;
streams.push(client.stream()?);
senders.push(client.sender());
}
loop {
let (message, index, _) =
futures::future::select_all(streams.iter_mut().map(|s| s.select_next_some())).await;
let message = message?;
let sender = &senders[index];
process_msg(sender, message)?;
}
}
fn process_msg(sender: &Sender, message: Message) -> error::Result<()> {
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains("pickles") {
sender.send_privmsg(target, "Hi!")?;
}
}
_ => (),
}
Ok(())
}

41
irc/examples/repeater.rs Normal file

@ -0,0 +1,41 @@
use futures::prelude::*;
use irc::client::prelude::*;
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()],
burst_window_length: Some(4),
max_messages_in_burst: Some(4),
..Default::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
let sender = client.sender();
loop {
let message = stream.select_next_some().await?;
if let Command::PRIVMSG(ref target, ref msg) = message.command {
if msg.starts_with(&*client.current_nickname()) {
let tokens: Vec<_> = msg.split(' ').collect();
if tokens.len() > 2 {
let n = tokens[0].len() + tokens[1].len() + 2;
if let Ok(count) = tokens[1].parse::<u8>() {
for _ in 0..count {
sender.send_privmsg(
message.response_target().unwrap_or(target),
&msg[n..],
)?;
}
}
}
}
}
}
}

33
irc/examples/simple.rs Normal file

@ -0,0 +1,33 @@
use futures::prelude::*;
use irc::client::prelude::*;
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()],
..Default::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
let sender = client.sender();
while let Some(message) = stream.next().await.transpose()? {
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains(client.current_nickname()) {
sender.send_privmsg(target, "Hi!")?;
}
}
_ => (),
}
}
Ok(())
}

@ -0,0 +1,35 @@
use futures::prelude::*;
use irc::client::prelude::*;
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("wumpscut".to_owned()),
server: Some("ircd.chat".to_owned()),
channels: vec!["#tcpdirect".to_owned()],
port: Some(6697),
use_tls: Some(true),
..Default::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
let sender = client.sender();
while let Some(message) = stream.next().await.transpose()? {
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains(client.current_nickname()) {
sender.send_privmsg(target, "oi")?;
}
}
_ => (),
}
}
Ok(())
}

@ -0,0 +1,36 @@
use futures::prelude::*;
use irc::client::prelude::*;
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()],
proxy_type: Some(ProxyType::Socks5),
proxy_server: Some("127.0.0.1".to_owned()),
proxy_port: Some(9050),
..Default::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
let sender = client.sender();
while let Some(message) = stream.next().await.transpose()? {
print!("{}", message);
match message.command {
Command::PRIVMSG(ref target, ref msg) => {
if msg.contains(client.current_nickname()) {
sender.send_privmsg(target, "Hi!")?;
}
}
_ => (),
}
}
Ok(())
}

24
irc/examples/tooter.rs Normal file

@ -0,0 +1,24 @@
use futures::prelude::*;
use irc::client::prelude::*;
use std::time::Duration;
// NOTE: this example is a conversion of `tweeter.rs` to an asynchronous style with `IrcReactor`.
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("mastodon".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()],
..Default::default()
};
let client = Client::from_config(config).await?;
let sender = client.sender();
let mut interval = tokio::time::interval(Duration::from_secs(1)).fuse();
loop {
let _ = interval.select_next_some().await;
sender.send_privmsg("#rust-spam", "AWOOOOOOOOOO")?;
}
}

31
irc/examples/tweeter.rs Normal file

@ -0,0 +1,31 @@
use futures::prelude::*;
use irc::client::prelude::*;
use std::time::Duration;
// NOTE: you can find an asynchronous version of this example with `IrcReactor` in `tooter.rs`.
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("pickles".to_owned()),
server: Some("irc.pdgn.co".to_owned()),
channels: vec!["#rust-spam".to_owned()],
..Default::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
let mut interval = tokio::time::interval(Duration::from_secs(10)).fuse();
loop {
futures::select! {
m = stream.select_next_some() => {
println!("{}", m?);
}
_ = interval.select_next_some() => {
client.send_privmsg("#rust-spam", "TWEET TWEET")?;
}
}
}
}

25
irc/irc-proto/Cargo.toml Normal file

@ -0,0 +1,25 @@
[package]
name = "irc-proto"
version = "0.14.0"
description = "The IRC protocol distilled."
authors = ["Aaron Weiss <awe@pdgn.co>"]
license = "MPL-2.0"
keywords = ["irc", "protocol", "tokio"]
categories = ["network-programming"]
documentation = "https://docs.rs/irc-proto/"
repository = "https://github.com/aatxe/irc"
edition = "2018"
[badges]
travis-ci = { repository = "aatxe/irc" }
[features]
default = ["bytes", "tokio", "tokio-util"]
[dependencies]
encoding = "0.2.0"
thiserror = "1.0.0"
bytes = { version = "0.5.0", optional = true }
tokio = { version = "0.2.0", optional = true }
tokio-util = { version = "0.3.0", features = ["codec"], optional = true }

99
irc/irc-proto/src/caps.rs Normal file

@ -0,0 +1,99 @@
//! Enumeration of all supported IRCv3 capability extensions.
/// List of all supported IRCv3 capability extensions from the
/// [IRCv3 specifications](http://ircv3.net/irc/).
#[derive(Debug, PartialEq)]
pub enum Capability {
/// [multi-prefix](http://ircv3.net/specs/extensions/multi-prefix-3.1.html)
MultiPrefix,
/// [sasl](http://ircv3.net/specs/extensions/sasl-3.1.html)
Sasl,
/// [account-notify](http://ircv3.net/specs/extensions/account-notify-3.1.html)
AccountNotify,
/// [away-notify](http://ircv3.net/specs/extensions/away-notify-3.1.html)
AwayNotify,
/// [extended-join](http://ircv3.net/specs/extensions/extended-join-3.1.html)
ExtendedJoin,
/// [metadata](http://ircv3.net/specs/core/metadata-3.2.html)
Metadata,
/// [metadata-notify](http://ircv3.net/specs/core/metadata-3.2.html)
MetadataNotify,
/// [monitor](http://ircv3.net/specs/core/monitor-3.2.html)
Monitor,
/// [account-tag](http://ircv3.net/specs/extensions/account-tag-3.2.html)
AccountTag,
/// [batch](http://ircv3.net/specs/extensions/batch-3.2.html)
Batch,
/// [cap-notify](http://ircv3.net/specs/extensions/cap-notify-3.2.html)
CapNotify,
/// [chghost](http://ircv3.net/specs/extensions/chghost-3.2.html)
ChgHost,
/// [echo-message](http://ircv3.net/specs/extensions/echo-message-3.2.html)
EchoMessage,
/// [invite-notify](http://ircv3.net/specs/extensions/invite-notify-3.2.html)
InviteNotify,
/// [server-time](http://ircv3.net/specs/extensions/server-time-3.2.html)
ServerTime,
/// [userhost-in-names](http://ircv3.net/specs/extensions/userhost-in-names-3.2.html)
UserhostInNames,
/// Custom IRCv3 capability extensions
Custom(&'static str),
}
/// List of IRCv3 capability negotiation versions.
pub enum NegotiationVersion {
/// [IRCv3.1](http://ircv3.net/specs/core/capability-negotiation-3.1.html)
V301,
/// [IRCv3.2](http://ircv3.net/specs/core/capability-negotiation-3.2.html)
V302,
}
impl AsRef<str> for Capability {
fn as_ref(&self) -> &str {
match *self {
Capability::MultiPrefix => "multi-prefix",
Capability::Sasl => "sasl",
Capability::AccountNotify => "account-notify",
Capability::AwayNotify => "away-notify",
Capability::ExtendedJoin => "extended-join",
Capability::Metadata => "metadata",
Capability::MetadataNotify => "metadata-notify",
Capability::Monitor => "monitor",
Capability::AccountTag => "account-tag",
Capability::Batch => "batch",
Capability::CapNotify => "cap-notify",
Capability::ChgHost => "chghost",
Capability::EchoMessage => "echo-message",
Capability::InviteNotify => "invite-notify",
Capability::ServerTime => "server-time",
Capability::UserhostInNames => "userhost-in-names",
Capability::Custom(s) => s,
}
}
}
#[cfg(test)]
mod test {
use super::Capability::*;
#[test]
fn to_str() {
assert_eq!(MultiPrefix.as_ref(), "multi-prefix");
assert_eq!(Sasl.as_ref(), "sasl");
assert_eq!(AccountNotify.as_ref(), "account-notify");
assert_eq!(AwayNotify.as_ref(), "away-notify");
assert_eq!(ExtendedJoin.as_ref(), "extended-join");
assert_eq!(Metadata.as_ref(), "metadata");
assert_eq!(MetadataNotify.as_ref(), "metadata-notify");
assert_eq!(Monitor.as_ref(), "monitor");
assert_eq!(AccountTag.as_ref(), "account-tag");
assert_eq!(Batch.as_ref(), "batch");
assert_eq!(CapNotify.as_ref(), "cap-notify");
assert_eq!(ChgHost.as_ref(), "chghost");
assert_eq!(EchoMessage.as_ref(), "echo-message");
assert_eq!(InviteNotify.as_ref(), "invite-notify");
assert_eq!(ServerTime.as_ref(), "server-time");
assert_eq!(UserhostInNames.as_ref(), "userhost-in-names");
assert_eq!(Custom("example").as_ref(), "example");
}
}

22
irc/irc-proto/src/chan.rs Normal file

@ -0,0 +1,22 @@
//! An extension trait that provides the ability to check if a string is a channel name.
/// An extension trait giving strings a function to check if they are a channel.
pub trait ChannelExt {
/// Returns true if the specified name is a channel name.
fn is_channel_name(&self) -> bool;
}
impl<'a> ChannelExt for &'a str {
fn is_channel_name(&self) -> bool {
self.starts_with('#')
|| self.starts_with('&')
|| self.starts_with('+')
|| self.starts_with('!')
}
}
impl ChannelExt for String {
fn is_channel_name(&self) -> bool {
(&self[..]).is_channel_name()
}
}

184
irc/irc-proto/src/colors.rs Normal file

@ -0,0 +1,184 @@
//! 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 crate::colors::FormattedStringExt;
use std::borrow::Cow;
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");
}
}
}

1162
irc/irc-proto/src/command.rs Normal file

Різницю між файлами не показано, бо вона завелика Завантажити різницю

@ -0,0 +1,76 @@
//! IRC protocol errors using `failure`.
use thiserror::Error;
/// A `Result` type for IRC `ProtocolErrors`.
pub type Result<T, E = ProtocolError> = ::std::result::Result<T, E>;
/// An IRC protocol error.
#[derive(Debug, Error)]
pub enum ProtocolError {
/// An internal I/O error.
#[error("an io error occurred")]
Io(#[source] std::io::Error),
/// Error for invalid messages.
#[error("invalid message: {}", string)]
InvalidMessage {
/// The string that failed to parse.
string: String,
/// The detailed message parsing error.
#[source]
cause: MessageParseError,
},
}
impl From<std::io::Error> for ProtocolError {
fn from(e: std::io::Error) -> ProtocolError {
ProtocolError::Io(e)
}
}
/// Errors that occur when parsing messages.
#[derive(Debug, Error)]
pub enum MessageParseError {
/// The message was empty.
#[error("empty message")]
EmptyMessage,
/// The command was invalid (i.e. missing).
#[error("invalid command")]
InvalidCommand,
/// The mode string was malformed.
#[error("invalid mode string: {}", string)]
InvalidModeString {
/// The invalid mode string.
string: String,
/// The detailed mode parsing error.
#[source]
cause: ModeParseError,
},
/// The subcommand used was invalid.
#[error("invalid {} subcommand: {}", cmd, sub)]
InvalidSubcommand {
/// The command whose invalid subcommand was referenced.
cmd: &'static str,
/// The invalid subcommand.
sub: String,
},
}
/// Errors that occur while parsing mode strings.
#[derive(Debug, Error)]
pub enum ModeParseError {
/// Invalid modifier used in a mode string (only + and - are valid).
#[error("invalid mode modifier: {}", modifier)]
InvalidModeModifier {
/// The invalid mode modifier.
modifier: char,
},
/// Missing modifier used in a mode string.
#[error("missing mode modifier")]
MissingModeModifier,
}

53
irc/irc-proto/src/irc.rs Normal file

@ -0,0 +1,53 @@
//! Implementation of IRC codec for Tokio.
use bytes::BytesMut;
use tokio_util::codec::{Decoder, Encoder};
use crate::error;
use crate::line::LineCodec;
use crate::message::Message;
/// An IRC codec built around an inner codec.
pub struct IrcCodec {
inner: LineCodec,
}
impl IrcCodec {
/// Creates a new instance of IrcCodec wrapping a LineCodec with the specific encoding.
pub fn new(label: &str) -> error::Result<IrcCodec> {
LineCodec::new(label).map(|codec| IrcCodec { inner: codec })
}
/// Sanitizes the input string by cutting up to (and including) the first occurence of a line
/// terminiating phrase (`\r\n`, `\r`, or `\n`). This is used in sending messages through the
/// codec to prevent the injection of additional commands.
pub fn sanitize(mut data: String) -> String {
// n.b. ordering matters here to prefer "\r\n" over "\r"
if let Some((pos, len)) = ["\r\n", "\r", "\n"]
.iter()
.flat_map(|needle| data.find(needle).map(|pos| (pos, needle.len())))
.min_by_key(|&(pos, _)| pos)
{
data.truncate(pos + len);
}
data
}
}
impl Decoder for IrcCodec {
type Item = Message;
type Error = error::ProtocolError;
fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<Message>> {
self.inner
.decode(src)
.and_then(|res| res.map_or(Ok(None), |msg| msg.parse::<Message>().map(Some)))
}
}
impl Encoder<Message> for IrcCodec {
type Error = error::ProtocolError;
fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> {
self.inner.encode(IrcCodec::sanitize(msg.to_string()), dst)
}
}

28
irc/irc-proto/src/lib.rs Normal file

@ -0,0 +1,28 @@
//! Support for the IRC protocol using Tokio.
#![warn(missing_docs)]
pub mod caps;
pub mod chan;
pub mod colors;
pub mod command;
pub mod error;
#[cfg(feature = "tokio")]
pub mod irc;
#[cfg(feature = "tokio")]
pub mod line;
pub mod message;
pub mod mode;
pub mod prefix;
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};
#[cfg(feature = "tokio")]
pub use self::irc::IrcCodec;
pub use self::message::Message;
pub use self::mode::{ChannelMode, Mode, UserMode};
pub use self::prefix::Prefix;
pub use self::response::Response;

87
irc/irc-proto/src/line.rs Normal file

@ -0,0 +1,87 @@
//! Implementation of line-delimiting codec for Tokio.
use std::io;
use bytes::BytesMut;
use encoding::label::encoding_from_whatwg_label;
use encoding::{DecoderTrap, EncoderTrap, EncodingRef};
use tokio_util::codec::{Decoder, Encoder};
use crate::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,
next_index: 0,
})
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
&format!("Attempted to use unknown codec {}.", label)[..],
)
.into()
})
}
}
impl Decoder for LineCodec {
type Item = String;
type Error = error::ProtocolError;
fn decode(&mut self, src: &mut BytesMut) -> error::Result<Option<String>> {
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(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) {
Ok(data) => Ok(Some(data)),
Err(data) => Err(io::Error::new(
io::ErrorKind::InvalidInput,
&format!("Failed to decode {} as {}.", data, self.encoding.name())[..],
)
.into()),
}
} 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)
}
}
}
impl Encoder<String> for LineCodec {
type Error = error::ProtocolError;
fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> {
// Encode the message using the codec's encoding.
let data: error::Result<Vec<u8>> = self
.encoding
.encode(&msg, EncoderTrap::Replace)
.map_err(|data| {
io::Error::new(
io::ErrorKind::InvalidInput,
&format!("Failed to encode {} as {}.", data, self.encoding.name())[..],
)
.into()
});
// Write the encoded message to the output buffer.
dst.extend(&data?);
Ok(())
}
}

@ -0,0 +1,550 @@
//! A module providing a data structure for messages to and from IRC servers.
use std::borrow::ToOwned;
use std::fmt::{Display, Formatter, Result as FmtResult, Write};
use std::str::FromStr;
use crate::chan::ChannelExt;
use crate::command::Command;
use crate::error;
use crate::error::{MessageParseError, ProtocolError};
use crate::prefix::Prefix;
/// A data structure representing an IRC message according to the protocol specification. It
/// consists of a collection of IRCv3 tags, a prefix (describing the source of the message), and
/// the protocol command. If the command is unknown, it is treated as a special raw command that
/// consists of a collection of arguments and the special suffix argument. Otherwise, the command
/// is parsed into a more useful form as described in [Command](../command/enum.Command.html).
#[derive(Clone, PartialEq, Debug)]
pub struct Message {
/// Message tags as defined by [IRCv3.2](http://ircv3.net/specs/core/message-tags-3.2.html).
/// These tags are used to add extended information to the given message, and are commonly used
/// in IRCv3 extensions to the IRC protocol.
pub tags: Option<Vec<Tag>>,
/// The message prefix (or source) as defined by [RFC 2812](http://tools.ietf.org/html/rfc2812).
pub prefix: Option<Prefix>,
/// The IRC command, parsed according to the known specifications. The command itself and its
/// arguments (including the special suffix argument) are captured in this component.
pub command: Command,
}
impl Message {
/// Creates a new message from the given components.
///
/// # Example
/// ```
/// # extern crate irc_proto;
/// # use irc_proto::Message;
/// # fn main() {
/// let message = Message::new(
/// Some("nickname!username@hostname"), "JOIN", vec!["#channel"]
/// ).unwrap();
/// # }
/// ```
pub fn new(
prefix: Option<&str>,
command: &str,
args: Vec<&str>,
) -> Result<Message, MessageParseError> {
Message::with_tags(None, prefix, command, args)
}
/// Creates a new IRCv3.2 message from the given components, including message tags. These tags
/// are used to add extended information to the given message, and are commonly used in IRCv3
/// extensions to the IRC protocol.
pub fn with_tags(
tags: Option<Vec<Tag>>,
prefix: Option<&str>,
command: &str,
args: Vec<&str>,
) -> Result<Message, error::MessageParseError> {
Ok(Message {
tags: tags,
prefix: prefix.map(|p| p.into()),
command: Command::new(command, args)?,
})
}
/// Gets the nickname of the message source, if it exists.
///
/// # Example
/// ```
/// # extern crate irc_proto;
/// # use irc_proto::Message;
/// # fn main() {
/// let message = Message::new(
/// Some("nickname!username@hostname"), "JOIN", vec!["#channel"]
/// ).unwrap();
/// assert_eq!(message.source_nickname(), Some("nickname"));
/// # }
/// ```
pub fn source_nickname(&self) -> Option<&str> {
// <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
// <servername> ::= <host>
self.prefix.as_ref().and_then(|p| match p {
Prefix::Nickname(name, _, _) => Some(&name[..]),
_ => None,
})
}
/// Gets the likely intended place to respond to this message.
/// If the type of the message is a `PRIVMSG` or `NOTICE` and the message is sent to a channel,
/// the result will be that channel. In all other cases, this will call `source_nickname`.
///
/// # Example
/// ```
/// # extern crate irc_proto;
/// # use irc_proto::Message;
/// # fn main() {
/// let msg1 = Message::new(
/// Some("ada"), "PRIVMSG", vec!["#channel", "Hi, everyone!"]
/// ).unwrap();
/// assert_eq!(msg1.response_target(), Some("#channel"));
/// let msg2 = Message::new(
/// Some("ada"), "PRIVMSG", vec!["betsy", "betsy: hi"]
/// ).unwrap();
/// assert_eq!(msg2.response_target(), Some("ada"));
/// # }
/// ```
pub fn response_target(&self) -> Option<&str> {
match self.command {
Command::PRIVMSG(ref target, _) if target.is_channel_name() => Some(target),
Command::NOTICE(ref target, _) if target.is_channel_name() => Some(target),
_ => self.source_nickname(),
}
}
/// Converts a Message into a String according to the IRC protocol.
///
/// # Example
/// ```
/// # extern crate irc_proto;
/// # use irc_proto::Message;
/// # fn main() {
/// let msg = Message::new(
/// Some("ada"), "PRIVMSG", vec!["#channel", "Hi, everyone!"]
/// ).unwrap();
/// assert_eq!(msg.to_string(), ":ada PRIVMSG #channel :Hi, everyone!\r\n");
/// # }
/// ```
pub fn to_string(&self) -> String {
let mut ret = String::new();
if let Some(ref tags) = self.tags {
ret.push('@');
for tag in tags {
ret.push_str(&tag.0);
if let Some(ref value) = tag.1 {
ret.push('=');
escape_tag_value(&mut ret, &value);
}
ret.push(';');
}
ret.pop();
ret.push(' ');
}
if let Some(ref prefix) = self.prefix {
write!(ret, ":{} ", prefix).unwrap();
}
let cmd: String = From::from(&self.command);
ret.push_str(&cmd);
ret.push_str("\r\n");
ret
}
}
impl From<Command> for Message {
fn from(cmd: Command) -> Message {
Message {
tags: None,
prefix: None,
command: cmd,
}
}
}
impl FromStr for Message {
type Err = ProtocolError;
fn from_str(s: &str) -> Result<Message, Self::Err> {
if s.is_empty() {
return Err(ProtocolError::InvalidMessage {
string: s.to_owned(),
cause: MessageParseError::EmptyMessage,
});
}
let mut state = s;
let tags = if state.starts_with('@') {
let tags = state.find(' ').map(|i| &state[1..i]);
state = state.find(' ').map_or("", |i| &state[i + 1..]);
tags.map(|ts| {
ts.split(';')
.filter(|s| !s.is_empty())
.map(|s: &str| {
let mut iter = s.splitn(2, '=');
let (fst, snd) = (iter.next(), iter.next());
let snd = snd.map(unescape_tag_value);
Tag(fst.unwrap_or("").to_owned(), snd)
})
.collect::<Vec<_>>()
})
} else {
None
};
let prefix = if state.starts_with(':') {
let prefix = state.find(' ').map(|i| &state[1..i]);
state = state.find(' ').map_or("", |i| &state[i + 1..]);
prefix
} else {
None
};
let line_ending_len = if state.ends_with("\r\n") {
"\r\n"
} else if state.ends_with('\r') {
"\r"
} else if state.ends_with('\n') {
"\n"
} else {
""
}
.len();
let suffix = if state.contains(" :") {
let suffix = state
.find(" :")
.map(|i| &state[i + 2..state.len() - line_ending_len]);
state = state.find(" :").map_or("", |i| &state[..i + 1]);
suffix
} else {
state = &state[..state.len() - line_ending_len];
None
};
let command = match state.find(' ').map(|i| &state[..i]) {
Some(cmd) => {
state = state.find(' ').map_or("", |i| &state[i + 1..]);
cmd
}
// If there's no arguments but the "command" starts with colon, it's not a command.
None if state.starts_with(':') => {
return Err(ProtocolError::InvalidMessage {
string: s.to_owned(),
cause: MessageParseError::InvalidCommand,
})
}
// If there's no arguments following the command, the rest of the state is the command.
None => {
let cmd = state;
state = "";
cmd
}
};
let mut args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect();
if let Some(suffix) = suffix {
args.push(suffix);
}
Message::with_tags(tags, prefix, command, args).map_err(|e| ProtocolError::InvalidMessage {
string: s.to_owned(),
cause: e,
})
}
}
impl<'a> From<&'a str> for Message {
fn from(s: &'a str) -> Message {
s.parse().unwrap()
}
}
impl Display for Message {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{}", self.to_string())
}
}
/// A message tag as defined by [IRCv3.2](http://ircv3.net/specs/core/message-tags-3.2.html).
/// It consists of a tag key, and an optional value for the tag. Each message can contain a number
/// of tags (in the string format, they are separated by semicolons). Tags are used to add extended
/// information to a message under IRCv3.
#[derive(Clone, PartialEq, Debug)]
pub struct Tag(pub String, pub Option<String>);
fn escape_tag_value(msg: &mut String, value: &str) {
for c in value.chars() {
match c {
';' => msg.push_str("\\:"),
' ' => msg.push_str("\\s"),
'\\' => msg.push_str("\\\\"),
'\r' => msg.push_str("\\r"),
'\n' => msg.push_str("\\n"),
c => msg.push(c),
}
}
}
fn unescape_tag_value(value: &str) -> String {
let mut unescaped = String::with_capacity(value.len());
let mut iter = value.chars();
while let Some(c) = iter.next() {
if c == '\\' {
match iter.next() {
Some(':') => unescaped.push(';'),
Some('s') => unescaped.push(' '),
Some('\\') => unescaped.push('\\'),
Some('r') => unescaped.push('\r'),
Some('n') => unescaped.push('\n'),
Some(c) => unescaped.push(c),
None => break,
}
} else {
unescaped.push(c);
}
}
unescaped
}
#[cfg(test)]
mod test {
use super::{Message, Tag};
use crate::command::Command::{Raw, PRIVMSG, QUIT};
#[test]
fn new() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!(
Message::new(None, "PRIVMSG", vec!["test", "Testing!"]).unwrap(),
message
)
}
#[test]
fn source_nickname() {
assert_eq!(
Message::new(None, "PING", vec!["data"])
.unwrap()
.source_nickname(),
None
);
assert_eq!(
Message::new(Some("irc.test.net"), "PING", vec!["data"])
.unwrap()
.source_nickname(),
None
);
assert_eq!(
Message::new(Some("test!test@test"), "PING", vec!["data"])
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(
Message::new(Some("test@test"), "PING", vec!["data"])
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(
Message::new(Some("test!test@irc.test.com"), "PING", vec!["data"])
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(
Message::new(Some("test!test@127.0.0.1"), "PING", vec!["data"])
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(
Message::new(Some("test@test.com"), "PING", vec!["data"])
.unwrap()
.source_nickname(),
Some("test")
);
assert_eq!(
Message::new(Some("test"), "PING", vec!["data"])
.unwrap()
.source_nickname(),
Some("test")
);
}
#[test]
fn to_string() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!(&message.to_string()[..], "PRIVMSG test Testing!\r\n");
let message = Message {
tags: None,
prefix: Some("test!test@test".into()),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
assert_eq!(
&message.to_string()[..],
":test!test@test PRIVMSG test :Still testing!\r\n"
);
}
#[test]
fn from_string() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!(
"PRIVMSG test :Testing!\r\n".parse::<Message>().unwrap(),
message
);
let message = Message {
tags: None,
prefix: Some("test!test@test".into()),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
assert_eq!(
":test!test@test PRIVMSG test :Still testing!\r\n"
.parse::<Message>()
.unwrap(),
message
);
let message = Message {
tags: Some(vec![
Tag(format!("aaa"), Some(format!("bbb"))),
Tag(format!("ccc"), None),
Tag(format!("example.com/ddd"), Some(format!("eee"))),
]),
prefix: Some("test!test@test".into()),
command: PRIVMSG(format!("test"), format!("Testing with tags!")),
};
assert_eq!(
"@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
tags!\r\n"
.parse::<Message>()
.unwrap(),
message
)
}
#[test]
fn from_string_atypical_endings() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
assert_eq!(
"PRIVMSG test :Testing!\r".parse::<Message>().unwrap(),
message
);
assert_eq!(
"PRIVMSG test :Testing!\n".parse::<Message>().unwrap(),
message
);
assert_eq!(
"PRIVMSG test :Testing!".parse::<Message>().unwrap(),
message
);
}
#[test]
fn from_and_to_string() {
let message =
"@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
tags!\r\n";
assert_eq!(message.parse::<Message>().unwrap().to_string(), message);
}
#[test]
fn to_message() {
let message = Message {
tags: None,
prefix: None,
command: PRIVMSG(format!("test"), format!("Testing!")),
};
let msg: Message = "PRIVMSG test :Testing!\r\n".into();
assert_eq!(msg, message);
let message = Message {
tags: None,
prefix: Some("test!test@test".into()),
command: PRIVMSG(format!("test"), format!("Still testing!")),
};
let msg: Message = ":test!test@test PRIVMSG test :Still testing!\r\n".into();
assert_eq!(msg, message);
}
#[test]
fn to_message_with_colon_in_arg() {
// Apparently, UnrealIRCd (and perhaps some others) send some messages that include
// colons within individual parameters. So, let's make sure it parses correctly.
let message = Message {
tags: None,
prefix: Some("test!test@test".into()),
command: Raw(
format!("COMMAND"),
vec![format!("ARG:test"), format!("Testing!")],
),
};
let msg: Message = ":test!test@test COMMAND ARG:test :Testing!\r\n".into();
assert_eq!(msg, message);
}
#[test]
fn to_message_no_prefix_no_args() {
let message = Message {
tags: None,
prefix: None,
command: QUIT(None),
};
let msg: Message = "QUIT\r\n".into();
assert_eq!(msg, message);
}
#[test]
#[should_panic]
fn to_message_invalid_format() {
let _: Message = ":invalid :message".into();
}
#[test]
fn to_message_tags_escapes() {
let msg = "@tag=\\:\\s\\\\\\r\\n\\a\\ :test PRIVMSG #test :test\r\n"
.parse::<Message>()
.unwrap();
let message = Message {
tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]),
prefix: Some("test".into()),
command: PRIVMSG("#test".to_string(), "test".to_string()),
};
assert_eq!(msg, message);
}
#[test]
fn to_string_tags_escapes() {
let msg = Message {
tags: Some(vec![Tag("tag".to_string(), Some("; \\\r\na".to_string()))]),
prefix: Some("test".into()),
command: PRIVMSG("#test".to_string(), "test".to_string()),
}
.to_string();
let message = "@tag=\\:\\s\\\\\\r\\na :test PRIVMSG #test test\r\n";
assert_eq!(msg, message);
}
}

326
irc/irc-proto/src/mode.rs Normal file

@ -0,0 +1,326 @@
//! A module defining an API for IRC user and channel modes.
use std::fmt;
use crate::command::Command;
use crate::error::MessageParseError;
use crate::error::MessageParseError::InvalidModeString;
use crate::error::ModeParseError::*;
/// A marker trait for different kinds of Modes.
pub trait ModeType: fmt::Display + fmt::Debug + Clone + PartialEq {
/// Creates a command of this kind.
fn mode(target: &str, modes: &[Mode<Self>]) -> Command;
/// Returns true if this mode takes an argument, and false otherwise.
fn takes_arg(&self) -> bool;
/// Creates a Mode from a given char.
fn from_char(c: char) -> Self;
}
/// User modes for the MODE command.
#[derive(Clone, Debug, PartialEq)]
pub enum UserMode {
/// a - user is flagged as away
Away,
/// i - marks a users as invisible
Invisible,
/// w - user receives wallops
Wallops,
/// r - restricted user connection
Restricted,
/// o - operator flag
Oper,
/// O - local operator flag
LocalOper,
/// s - marks a user for receipt of server notices
ServerNotices,
/// x - masked hostname
MaskedHost,
/// Any other unknown-to-the-crate mode.
Unknown(char),
}
impl ModeType for UserMode {
fn mode(target: &str, modes: &[Mode<Self>]) -> Command {
Command::UserMODE(target.to_owned(), modes.to_owned())
}
fn takes_arg(&self) -> bool {
false
}
fn from_char(c: char) -> UserMode {
use self::UserMode::*;
match c {
'a' => Away,
'i' => Invisible,
'w' => Wallops,
'r' => Restricted,
'o' => Oper,
'O' => LocalOper,
's' => ServerNotices,
'x' => MaskedHost,
_ => Unknown(c),
}
}
}
impl fmt::Display for UserMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::UserMode::*;
write!(
f,
"{}",
match *self {
Away => 'a',
Invisible => 'i',
Wallops => 'w',
Restricted => 'r',
Oper => 'o',
LocalOper => 'O',
ServerNotices => 's',
MaskedHost => 'x',
Unknown(c) => c,
}
)
}
}
/// Channel modes for the MODE command.
#[derive(Clone, Debug, PartialEq)]
pub enum ChannelMode {
/// b - ban the user from joining or speaking in the channel
Ban,
/// e - exemptions from bans
Exception,
/// l - limit the maximum number of users in a channel
Limit,
/// i - channel becomes invite-only
InviteOnly,
/// I - exception to invite-only rule
InviteException,
/// k - specify channel key
Key,
/// m - channel is in moderated mode
Moderated,
/// r - entry for registered users only
RegisteredOnly,
/// s - channel is hidden from listings
Secret,
/// t - require permissions to edit topic
ProtectedTopic,
/// n - users must join channels to message them
NoExternalMessages,
/// q - user gets founder permission
Founder,
/// a - user gets admin or protected permission
Admin,
/// o - user gets oper permission
Oper,
/// h - user gets halfop permission
Halfop,
/// v - user gets voice permission
Voice,
/// Any other unknown-to-the-crate mode.
Unknown(char),
}
impl ModeType for ChannelMode {
fn mode(target: &str, modes: &[Mode<Self>]) -> Command {
Command::ChannelMODE(target.to_owned(), modes.to_owned())
}
fn takes_arg(&self) -> bool {
use self::ChannelMode::*;
match *self {
Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop
| Voice => true,
_ => false,
}
}
fn from_char(c: char) -> ChannelMode {
use self::ChannelMode::*;
match c {
'b' => Ban,
'e' => Exception,
'l' => Limit,
'i' => InviteOnly,
'I' => InviteException,
'k' => Key,
'm' => Moderated,
'r' => RegisteredOnly,
's' => Secret,
't' => ProtectedTopic,
'n' => NoExternalMessages,
'q' => Founder,
'a' => Admin,
'o' => Oper,
'h' => Halfop,
'v' => Voice,
_ => Unknown(c),
}
}
}
impl fmt::Display for ChannelMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::ChannelMode::*;
write!(
f,
"{}",
match *self {
Ban => 'b',
Exception => 'e',
Limit => 'l',
InviteOnly => 'i',
InviteException => 'I',
Key => 'k',
Moderated => 'm',
RegisteredOnly => 'r',
Secret => 's',
ProtectedTopic => 't',
NoExternalMessages => 'n',
Founder => 'q',
Admin => 'a',
Oper => 'o',
Halfop => 'h',
Voice => 'v',
Unknown(c) => c,
}
)
}
}
/// A mode argument for the MODE command.
#[derive(Clone, Debug, PartialEq)]
pub enum Mode<T>
where
T: ModeType,
{
/// Adding the specified mode, optionally with an argument.
Plus(T, Option<String>),
/// Removing the specified mode, optionally with an argument.
Minus(T, Option<String>),
}
impl<T> Mode<T>
where
T: ModeType,
{
/// Creates a plus mode with an `&str` argument.
pub fn plus(inner: T, arg: Option<&str>) -> Mode<T> {
Mode::Plus(inner, arg.map(|s| s.to_owned()))
}
/// Creates a minus mode with an `&str` argument.
pub fn minus(inner: T, arg: Option<&str>) -> Mode<T> {
Mode::Minus(inner, arg.map(|s| s.to_owned()))
}
}
impl<T> fmt::Display for Mode<T>
where
T: ModeType,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Mode::Plus(ref mode, Some(ref arg)) => write!(f, "{}{} {}", "+", mode, arg),
Mode::Minus(ref mode, Some(ref arg)) => write!(f, "{}{} {}", "-", mode, arg),
Mode::Plus(ref mode, None) => write!(f, "{}{}", "+", mode),
Mode::Minus(ref mode, None) => write!(f, "{}{}", "-", mode),
}
}
}
enum PlusMinus {
Plus,
Minus,
}
// MODE user [modes]
impl Mode<UserMode> {
// TODO: turning more edge cases into errors.
/// Parses the specified mode string as user modes.
pub fn as_user_modes(pieces: &[&str]) -> Result<Vec<Mode<UserMode>>, MessageParseError> {
parse_modes(pieces)
}
}
// MODE channel [modes [modeparams]]
impl Mode<ChannelMode> {
// TODO: turning more edge cases into errors.
/// Parses the specified mode string as channel modes.
pub fn as_channel_modes(pieces: &[&str]) -> Result<Vec<Mode<ChannelMode>>, MessageParseError> {
parse_modes(pieces)
}
}
fn parse_modes<T>(pieces: &[&str]) -> Result<Vec<Mode<T>>, MessageParseError>
where
T: ModeType,
{
use self::PlusMinus::*;
let mut res = vec![];
if let Some((first, rest)) = pieces.split_first() {
let mut modes = first.chars();
let mut args = rest.iter();
let mut cur_mod = match modes.next() {
Some('+') => Plus,
Some('-') => Minus,
Some(c) => {
return Err(InvalidModeString {
string: pieces.join(" ").to_owned(),
cause: InvalidModeModifier { modifier: c },
})
}
None => {
return Err(InvalidModeString {
string: pieces.join(" ").to_owned(),
cause: MissingModeModifier,
})
}
};
for c in modes {
match c {
'+' => cur_mod = Plus,
'-' => cur_mod = Minus,
_ => {
let mode = T::from_char(c);
let arg = if mode.takes_arg() {
// TODO: if there's no arg, this should error
args.next()
} else {
None
};
res.push(match cur_mod {
Plus => Mode::Plus(mode, arg.map(|s| s.to_string())),
Minus => Mode::Minus(mode, arg.map(|s| s.to_string())),
})
}
}
}
// TODO: if there are extra args left, this should error
Ok(res)
} else {
Err(InvalidModeString {
string: pieces.join(" ").to_owned(),
cause: MissingModeModifier,
})
}
}

207
irc/irc-proto/src/prefix.rs Normal file

@ -0,0 +1,207 @@
//! A module providing an enum for a message prefix.
use std::fmt;
use std::str::FromStr;
/// The Prefix indicates "the true origin of the message", according to the server.
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum Prefix {
/// servername, e.g. collins.mozilla.org
ServerName(String),
/// nickname [ ["!" username] "@" hostname ]
/// i.e. Nickname(nickname, username, hostname)
/// Any of the strings may be ""
Nickname(String, String, String),
}
impl Prefix {
/// Creates a prefix by parsing a string.
///
/// # Example
/// ```
/// # extern crate irc_proto;
/// # use irc_proto::Prefix;
/// # fn main() {
/// Prefix::new_from_str("nickname!username@hostname");
/// Prefix::new_from_str("example.com");
/// # }
/// ```
pub fn new_from_str(s: &str) -> Prefix {
#[derive(Copy, Clone, Eq, PartialEq)]
enum Active {
Name,
User,
Host,
}
let mut name = String::new();
let mut user = String::new();
let mut host = String::new();
let mut active = Active::Name;
let mut is_server = false;
for c in s.chars() {
if c == '.' && active == Active::Name {
// We won't return Nickname("nick", "", "") but if @ or ! are
// encountered, then we set this back to false
is_server = true;
}
match c {
'!' if active == Active::Name => {
is_server = false;
active = Active::User;
}
'@' if active != Active::Host => {
is_server = false;
active = Active::Host;
}
_ => {
// Push onto the active buffer
match active {
Active::Name => &mut name,
Active::User => &mut user,
Active::Host => &mut host,
}
.push(c)
}
}
}
if is_server {
Prefix::ServerName(name)
} else {
Prefix::Nickname(name, user, host)
}
}
}
/// This implementation never returns an error and is isomorphic with `Display`.
impl FromStr for Prefix {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Prefix::new_from_str(s))
}
}
/// This is isomorphic with `FromStr`
impl fmt::Display for Prefix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Prefix::ServerName(name) => write!(f, "{}", name),
Prefix::Nickname(name, user, host) => match (&name[..], &user[..], &host[..]) {
("", "", "") => write!(f, ""),
(name, "", "") => write!(f, "{}", name),
(name, user, "") => write!(f, "{}!{}", name, user),
(name, "", host) => write!(f, "{}@{}", name, host),
(name, user, host) => write!(f, "{}!{}@{}", name, user, host),
},
}
}
}
impl<'a> From<&'a str> for Prefix {
fn from(s: &str) -> Self {
Prefix::new_from_str(s)
}
}
#[cfg(test)]
mod test {
use super::Prefix::{self, Nickname, ServerName};
// Checks that str -> parsed -> Display doesn't lose data
fn test_parse(s: &str) -> Prefix {
let prefix = Prefix::new_from_str(s);
let s2 = format!("{}", prefix);
assert_eq!(s, &s2);
prefix
}
#[test]
fn print() {
let s = format!("{}", Nickname("nick".into(), "".into(), "".into()));
assert_eq!(&s, "nick");
let s = format!("{}", Nickname("nick".into(), "user".into(), "".into()));
assert_eq!(&s, "nick!user");
let s = format!("{}", Nickname("nick".into(), "user".into(), "host".into()));
assert_eq!(&s, "nick!user@host");
}
#[test]
fn parse_word() {
assert_eq!(
test_parse("only_nick"),
Nickname("only_nick".into(), String::new(), String::new())
)
}
#[test]
fn parse_host() {
assert_eq!(test_parse("host.tld"), ServerName("host.tld".into()))
}
#[test]
fn parse_nick_user() {
assert_eq!(
test_parse("test!nick"),
Nickname("test".into(), "nick".into(), String::new())
)
}
#[test]
fn parse_nick_user_host() {
assert_eq!(
test_parse("test!nick@host"),
Nickname("test".into(), "nick".into(), "host".into())
)
}
#[test]
fn parse_dot_and_symbols() {
assert_eq!(
test_parse("test.net@something"),
Nickname("test.net".into(), "".into(), "something".into())
)
}
#[test]
fn parse_danger_cases() {
assert_eq!(
test_parse("name@name!user"),
Nickname("name".into(), "".into(), "name!user".into())
);
assert_eq!(
// can't reverse the parse
"name!@".parse::<Prefix>().unwrap(),
Nickname("name".into(), "".into(), "".into())
);
assert_eq!(
// can't reverse the parse
"name!@hostname".parse::<Prefix>().unwrap(),
Nickname("name".into(), "".into(), "hostname".into())
);
assert_eq!(
test_parse("name!.user"),
Nickname("name".into(), ".user".into(), "".into())
);
assert_eq!(
test_parse("name!user.user"),
Nickname("name".into(), "user.user".into(), "".into())
);
assert_eq!(
test_parse("name!user@host.host"),
Nickname("name".into(), "user".into(), "host.host".into())
);
assert_eq!(
test_parse("!user"),
Nickname("".into(), "user".into(), "".into())
);
assert_eq!(
"!@host.host".parse::<Prefix>().unwrap(),
Nickname("".into(), "".into(), "host.host".into())
);
}
}

@ -0,0 +1,443 @@
//! Enumeration of all the possible server responses.
#![allow(non_camel_case_types)]
use std::str::FromStr;
macro_rules! make_response {
($($(#[$attr:meta])+ $variant:ident = $value:expr),+) => {
/// List of all server responses as defined in
/// [RFC 2812](http://tools.ietf.org/html/rfc2812) and
/// [Modern docs](https://modern.ircdocs.horse/#numerics) (henceforth referred to as
/// Modern). All commands are documented with their expected form from the RFC, and any
/// useful, additional information about the response code.
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(u16)]
pub enum Response {
$($(#[$attr])+ $variant = $value),+
}
impl Response {
/// Generates a Response from a u16.
fn from_u16(val: u16) -> Option<Response> {
match val {
$($value => Some(Response::$variant),)+
_ => None
}
}
}
}
}
make_response! {
// Expected replies
/// `001 Welcome to the Internet Relay Network <nick>!<user>@<host>` (Source: RFC2812)
RPL_WELCOME = 1,
/// `002 Your host is <servername>, running version <ver>` (Source: RFC2812)
RPL_YOURHOST = 2,
/// `003 This server was created <date>` (Source: RFC2812)
RPL_CREATED = 3,
/// `004 <servername> <version> <available user modes> <available channel modes>` (Source:
/// RFC2812)
///
/// Various IRCds may choose to include additional arguments to `RPL_MYINFO`, and it's best to
/// check for certain what the servers you're targeting do. Typically, there are additional
/// parameters at the end for modes that have parameters, and server modes.
RPL_MYINFO = 4,
/// `005 <servername> *(<feature>(=<value>)) :are supported by this server` (Source: Modern)
///
/// [RPL_ISUPPORT](https://modern.ircdocs.horse/#rplisupport-005) replaces RPL_BOUNCE from
/// RFC2812, but does so consistently in modern IRCd implementations. RPL_BOUNCE has been moved
/// to `010`.
RPL_ISUPPORT = 5,
/// `010 Try server <server name>, port <port number>` (Source: Modern)
RPL_BOUNCE = 10,
/// Undefined format. (Source: Modern)
///
/// RPL_NONE is a dummy numeric. It does not have a defined use nor format.
RPL_NONE = 300,
/// `302 :*1<reply> *( " " <reply> )` (Source: RFC2812)
RPL_USERHOST = 302,
/// `303 :*1<nick> *( " " <nick> )` (Source: RFC2812)
RPL_ISON = 303,
/// `301 <nick> :<away message>` (Source: RFC2812)
RPL_AWAY = 301,
/// `305 :You are no longer marked as being away` (Source: RFC2812)
RPL_UNAWAY = 305,
/// `306 :You have been marked as being away` (Source: RFC2812)
RPL_NOWAWAY = 306,
/// `311 <nick> <user> <host> * :<real name>` (Source: RFC2812)
RPL_WHOISUSER = 311,
/// `312 <nick> <server> :<server info>` (Source: RFC2812)
RPL_WHOISSERVER = 312,
/// `313 <nick> :is an IRC operator` (Source: RFC2812)
RPL_WHOISOPERATOR = 313,
/// `317 <nick> <integer> :seconds idle` (Source: RFC2812)
RPL_WHOISIDLE = 317,
/// `318 <nick> :End of WHOIS list` (Source: RFC2812)
RPL_ENDOFWHOIS = 318,
/// `319 <nick> :*( ( "@" / "+" ) <channel> " " )` (Source: RFC2812)
RPL_WHOISCHANNELS = 319,
/// `314 <nick> <user> <host> * :<real name>` (Source: RFC2812)
RPL_WHOWASUSER = 314,
/// `369 <nick> :End of WHOWAS` (Source: RFC2812)
RPL_ENDOFWHOWAS = 369,
/// Obsolete. Not used. (Source: RFC2812)
RPL_LISTSTART = 321,
/// `322 <channel> <# visible> :<topic>` (Source: RFC2812)
RPL_LIST = 322,
/// `323 :End of LIST (Source: RFC2812)
RPL_LISTEND = 323,
/// `325 <channel> <nickname>` (Source: RFC2812)
RPL_UNIQOPIS = 325,
/// `324 <channel> <mode> <mode params>` (Source: RFC2812)
RPL_CHANNELMODEIS = 324,
/// `331 <channel> :No topic is set` (Source: RFC2812)
RPL_NOTOPIC = 331,
/// `332 <channel> :<topic>` (Source: RFC2812)
RPL_TOPIC = 332,
/// `333 <channel> <nick>!<user>@<host> <unix timestamp>` (Source: RFC2812)
RPL_TOPICWHOTIME = 333,
/// `341 <channel> <nick>` (Source: RFC2812)
RPL_INVITING = 341,
/// `342 <user> :Summoning user to IRC` (Source: RFC2812)
///
/// According to Modern, this response is rarely implemented. In practice, people simply message
/// one another in a channel with their specified username in the message, rather than use the
/// `SUMMON` command.
RPL_SUMMONING = 342,
/// `346 <channel> <invitemask>` (Source: RFC2812)
RPL_INVITELIST = 346,
/// `347 <channel> :End of channel invite list` (Source: RFC2812)
///
/// According to Modern, `RPL_ENDOFEXCEPTLIST` (349) is frequently deployed for this same
/// purpose and the difference will be noted in channel mode and the statement in the suffix.
RPL_ENDOFINVITELIST = 347,
/// `348 <channel> <exceptionmask>` (Source: RFC2812)
RPL_EXCEPTLIST = 348,
/// `349 <channel> :End of channel exception list` (Source: RFC2812)
RPL_ENDOFEXCEPTLIST = 349,
/// `351 <version> <server> :<comments>` (Source: RFC2812/Modern)
RPL_VERSION = 351,
/// `352 <channel> <user> <host> <server> <nick> ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
/// :<hopcount> <real name>` (Source: RFC2812)
RPL_WHOREPLY = 352,
/// `315 <name> :End of WHO list` (Source: RFC2812)
RPL_ENDOFWHO = 315,
/// `353 ( "=" / "*" / "@" ) <channel> :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )`
/// (Source: RFC2812)
RPL_NAMREPLY = 353,
/// `366 <channel> :End of NAMES list` (Source: RFC2812)
RPL_ENDOFNAMES = 366,
/// `364 <mask> <server> :<hopcount> <server info>` (Source: RFC2812)
RPL_LINKS = 364,
/// `365 <mask> :End of LINKS list` (Source: RFC2812)
RPL_ENDOFLINKS = 365,
/// `367 <channel> <banmask>` (Source: RFC2812)
RPL_BANLIST = 367,
/// `368 <channel> :End of channel ban list` (Source: RFC2812)
RPL_ENDOFBANLIST = 368,
/// `371 :<string>` (Source: RFC2812)
RPL_INFO = 371,
/// `374 :End of INFO list` (Source: RFC2812)
RPL_ENDOFINFO = 374,
/// `375 :- <server> Message of the day -` (Source: RFC2812)
RPL_MOTDSTART = 375,
/// `372 :- <text>` (Source: RFC2812)
RPL_MOTD = 372,
/// `376 :End of MOTD command` (Source: RFC2812)
RPL_ENDOFMOTD = 376,
/// `381 :You are now an IRC operator` (Source: RFC2812)
RPL_YOUREOPER = 381,
/// `382 <config file> :Rehashing` (Source: RFC2812)
RPL_REHASHING = 382,
/// `383 You are service <servicename>` (Source: RFC2812)
RPL_YOURESERVICE = 383,
/// `391 <server> :<string showing server's local time>` (Source: RFC2812)
RPL_TIME = 391,
/// `392 :UserID Terminal Host` (Source: RFC2812)
RPL_USERSSTART = 392,
/// `393 :<username> <ttyline> <hostname>` (Source: RFC2812)
RPL_USERS = 393,
/// `394 :End of users` (Source: RFC2812)
RPL_ENDOFUSERS = 394,
/// `395 :Nobody logged in` (Source: RFC2812)
RPL_NOUSERS = 395,
/// `396 <nickname> <host> :is now your displayed host` (Source: InspIRCd)
///
/// This response code is sent after a user enables the user mode +x (host masking), and it is
/// successfully enabled. The particular format described above is from InspIRCd, but the
/// response code should be common amongst servers that support host masks.
RPL_HOSTHIDDEN = 396,
/// `200 Link <version & debug level> <destination> <next server> V<protocol version>
/// <link uptime in seconds> <backstream sendq> <upstream sendq>` (Source: RFC2812)
RPL_TRACELINK = 200,
/// `201 Try. <class> <server>` (Source: RFC2812)
RPL_TRACECONNECTING = 201,
/// `202 H.S. <class> <server>` (Source: RFC2812)
RPL_TRACEHANDSHAKE = 202,
/// `203 ???? <class> [<client IP address in dot form>]` (Source: RFC2812)
RPL_TRACEUKNOWN = 203,
/// `204 Oper <class> <nick>` (Source: RFC2812)
RPL_TRACEOPERATOR = 204,
/// `205 User <class> <nick>` (Source: RFC2812)
RPL_TRACEUSER = 205,
/// `206 Serv <class> <int>S <int>C <server> <nick!user|*!*>@<host|server> V<protocol version>`
/// (Source: RFC2812)
RPL_TRACESERVER = 206,
/// `207 Service <class> <name> <type> <active type>` (Source: RFC2812)
RPL_TRACESERVICE = 207,
/// `208 <newtype> 0 <client name>` (Source: RFC2812)
RPL_TRACENEWTYPE = 208,
/// `209 Class <class> <count>` (Source: RFC2812)
RPL_TRACECLASS = 209,
/// Unused. (Source: RFC2812)
RPL_TRACERECONNECT = 210,
/// `261 File <logfile> <debug level>` (Source: RFC2812)
RPL_TRACELOG = 261,
/// `262 <server name> <version & debug level> :End of TRACE` (Source: RFC2812)
RPL_TRACEEND = 262,
/// `211 <linkname> <sendq> <sent messages> <sent Kbytes> <received messages> <received Kbytes>
/// <time open>` (Source: RFC2812)
RPL_STATSLINKINFO = 211,
/// `212 <command> <count> <byte count> <remote count>` (Source: RFC2812)
RPL_STATSCOMMANDS = 212,
/// `219 <stats letter> :End of STATS report` (Source: RFC2812)
RPL_ENDOFSTATS = 219,
/// `242 :Server Up %d days %d:%02d:%02d` (Source: RFC2812)
RPL_STATSUPTIME = 242,
/// `243 O <hostmask> * <name>` (Source: RFC2812)
RPL_STATSOLINE = 243,
/// `221 <user mode string>` (Source: RFC2812)
RPL_UMODEIS = 221,
/// `234 <name> <server> <mask> <type> <hopcount> <info>` (Source: RFC2812)
RPL_SERVLIST = 234,
/// `235 <mask> <type> :End of service listing` (Source: RFC2812)
RPL_SERVLISTEND = 235,
/// `251 :There are <int> users and <int> services on <int> servers` (Source: RFC2812)
RPL_LUSERCLIENT = 251,
/// `252 <integer> :operator(s) online` (Source: RFC2812)
RPL_LUSEROP = 252,
/// `253 <integer> :unknown connection(s)` (Source: RFC2812)
RPL_LUSERUNKNOWN = 253,
/// `254 <integer> :channels formed` (Source: RFC2812)
RPL_LUSERCHANNELS = 254,
/// `255 :I have <integer> clients and <integer> servers` (Source: RFC2812)
RPL_LUSERME = 255,
/// `256 <server> :Administrative info` (Source: RFC2812)
RPL_ADMINME = 256,
/// `257 :<admin info>` (Source: RFC2812)
RPL_ADMINLOC1 = 257,
/// `258 :<admin info>` (Source: RFC2812)
RPL_ADMINLOC2 = 258,
/// `259 :<admin info>` (Source: RFC2812)
RPL_ADMINEMAIL = 259,
/// `263 <command> :Please wait a while and try again.` (Source: RFC2812)
RPL_TRYAGAIN = 263,
/// `265 <client> [<u> <m>] :Current local users <u>, max <m>` (Source: Modern)
RPL_LOCALUSERS = 265,
/// `266 <client> [<u> <m>] :Current local users <u>, max <m>` (Source: Modern)
RPL_GLOBALUSERS = 266,
/// `276 <client> <nick> :has client certificate fingerprint <fingerprint>` (Source: Modern)
RPL_WHOISCERTFP = 276,
/// `730 <nick> :target[,target2]*` (Source: RFC2812)
RPL_MONONLINE = 730,
/// `731 <nick> :target[,target2]*` (Source: RFC2812)
RPL_MONOFFLINE = 731,
/// `732 <nick> :target[,target2]*` (Source: RFC2812)
RPL_MONLIST = 732,
/// `733 <nick> :End of MONITOR list` (Source: RFC2812)
RPL_ENDOFMONLIST = 733,
/// `760 <target> <key> <visibility> :<value>` (Source: RFC2812)
RPL_WHOISKEYVALUE = 760,
/// `761 <target> <key> <visibility> :[<value>]` (Source: RFC2812)
RPL_KEYVALUE = 761,
/// `762 :end of metadata` (Source: RFC2812)
RPL_METADATAEND = 762,
/// `900 <nick> <nick>!<ident>@<host> <account> :You are now logged in as <user>` (Source:
/// IRCv3)
RPL_LOGGEDIN = 900,
/// `901 <nick> <nick>!<ident>@<host> :You are now logged out` (Source: IRCv3)
RPL_LOGGEDOUT = 901,
/// `903 <nick> :SASL authentication successful` (Source: IRCv3)
RPL_SASLSUCCESS = 903,
/// `908 <nick> <mechanisms> :are available SASL mechanisms` (Source: IRCv3)
RPL_SASLMECHS = 908,
// Error replies
/// `400 <client> <command>{ <subcommand>} :<info>` (Source: Modern)
///
/// According to Modern, this error will be returned when the given command/subcommand could not
/// be processed. It's a very general error, and should only be used when more specific numerics
/// do not suffice.
ERR_UNKNOWNERROR = 400,
/// `401 <nickname> :No such nick/channel` (Source: RFC2812)
ERR_NOSUCHNICK = 401,
/// `402 <server name> :No such server` (Source: RFC2812)
ERR_NOSUCHSERVER = 402,
/// `403 <channel name> :No such channel` (Source: RFC2812)
ERR_NOSUCHCHANNEL = 403,
/// `404 <channel name> :Cannot send to channel` (Source: RFC2812)
ERR_CANNOTSENDTOCHAN = 404,
/// `405 <channel name> :You have joined too many channels` (Source: RFC2812)
ERR_TOOMANYCHANNELS = 405,
/// `406 <nickname> :There was no such nickname` (Source: RFC2812)
ERR_WASNOSUCHNICK = 406,
/// `407 <target> :<error code> recipients. <abort message>` (Source: RFC2812)
ERR_TOOMANYTARGETS = 407,
/// `408 <service name> :No such service` (Source: RFC2812)
ERR_NOSUCHSERVICE = 408,
/// `409 :No origin specified` (Source: RFC2812)
ERR_NOORIGIN = 409,
/// `411 :No recipient given (<command>)` (Source: RFC2812)
ERR_NORECIPIENT = 411,
/// `412 :No text to send` (Source: RFC2812)
ERR_NOTEXTTOSEND = 412,
/// `413 <mask> :No toplevel domain specified` (Source: RFC2812)
ERR_NOTOPLEVEL = 413,
/// `414 <mask> :Wildcard in toplevel domain` (Source: RFC2812)
ERR_WILDTOPLEVEL = 414,
/// `415 <mask> :Bad Server/host mask` (Source: RFC2812)
ERR_BADMASK = 415,
/// `421 <command> :Unknown command` (Source: RFC2812)
ERR_UNKNOWNCOMMAND = 421,
/// `422 :MOTD File is missing` (Source: RFC2812)
ERR_NOMOTD = 422,
/// `423 <server> :No administrative info available` (Source: RFC2812)
ERR_NOADMININFO = 423,
/// `424 :File error doing <file op> on <file>` (Source: RFC2812)
ERR_FILEERROR = 424,
/// `431 :No nickname given` (Source: RFC2812)
ERR_NONICKNAMEGIVEN = 431,
/// `432 <nick> :Erroneous nickname"` (Source: RFC2812)
ERR_ERRONEOUSNICKNAME = 432,
/// `433 <nick> :Nickname is already in use` (Source: RFC2812)
ERR_NICKNAMEINUSE = 433,
/// `436 <nick> :Nickname collision KILL from <user>@<host>` (Source: RFC2812)
ERR_NICKCOLLISION = 436,
/// `437 <nick/channel> :Nick/channel is temporarily unavailable` (Source: RFC2812)
ERR_UNAVAILRESOURCE = 437,
/// `441 <nick> <channel> :They aren't on that channel` (Source: RFC2812)
ERR_USERNOTINCHANNEL = 441,
/// `442 <channel> :You're not on that channel` (Source: RFC2812)
ERR_NOTONCHANNEL = 442,
/// `443 <user> <channel> :is already on channel` (Source: RFC2812)
ERR_USERONCHANNEL = 443,
/// `444 <user> :User not logged in` (Source: RFC2812)
ERR_NOLOGIN = 444,
/// `445 :SUMMON has been disabled` (Source: RFC2812)
ERR_SUMMONDISABLED = 445,
/// `446 :USERS has been disabled` (Source: RFC2812)
ERR_USERSDISABLED = 446,
/// `451 :You have not registered` (Source: RFC2812)
ERR_NOTREGISTERED = 451,
/// `461 <command> :Not enough parameters` (Source: RFC2812)
ERR_NEEDMOREPARAMS = 461,
/// `462 :Unauthorized command (already registered)` (Source: RFC2812)
ERR_ALREADYREGISTRED = 462,
/// `463 :Your host isn't among the privileged` (Source: RFC2812)
ERR_NOPERMFORHOST = 463,
/// `464 :Password incorrect` (Source: RFC2812)
ERR_PASSWDMISMATCH = 464,
/// `465 :You are banned from this server` (Source: RFC2812)
ERR_YOUREBANNEDCREEP = 465,
/// `466` (Source: RFC2812)
ERR_YOUWILLBEBANNED = 466,
/// `467 <channel> :Channel key already set` (Source: RFC2812)
ERR_KEYSET = 467,
/// `471 <channel> :Cannot join channel (+l)` (Source: RFC2812)
ERR_CHANNELISFULL = 471,
/// `472 <char> :is unknown mode char to me for <channel>` (Source: RFC2812)
ERR_UNKNOWNMODE = 472,
/// `473 <channel> :Cannot join channel (+i)` (Source: RFC2812)
ERR_INVITEONLYCHAN = 473,
/// `474 <channel> :Cannot join channel (+b)` (Source: RFC2812)
ERR_BANNEDFROMCHAN = 474,
/// `475 <channel> :Cannot join channel (+k)` (Source: RFC2812)
ERR_BADCHANNELKEY = 475,
/// `476 <channel> :Bad Channel Mask` (Source: RFC2812)
ERR_BADCHANMASK = 476,
/// `477 <channel> :Channel doesn't support modes` (Source: RFC2812)
ERR_NOCHANMODES = 477,
/// `478 <channel> <char> :Channel list is full` (Source: RFC2812)
ERR_BANLISTFULL = 478,
/// `481 :Permission Denied- You're not an IRC operator` (Source: RFC2812)
ERR_NOPRIVILEGES = 481,
/// `482 <channel> :You're not channel operator` (Source: RFC2812)
ERR_CHANOPRIVSNEEDED = 482,
/// `483 :You can't kill a server!` (Source: RFC2812)
ERR_CANTKILLSERVER = 483,
/// `484 :Your connection is restricted!` (Source: RFC2812)
ERR_RESTRICTED = 484,
/// `485 :You're not the original channel operator` (Source: RFC2812)
ERR_UNIQOPPRIVSNEEDED = 485,
/// `491 :No O-lines for your host` (Source: RFC2812)
ERR_NOOPERHOST = 491,
/// `501 :Unknown MODE flag` (Source: RFC2812)
ERR_UMODEUNKNOWNFLAG = 501,
/// `502 :Cannot change mode for other users` (Source: RFC2812)
ERR_USERSDONTMATCH = 502,
/// `723 <client> <priv> :Insufficient oper privileges.` (Source: Modern)
///
/// Sent to an operator to indicate that they don't have the specific privileges to perform the
/// desired action. The format and meaning of the privilege string is server-defined.
ERR_NOPRIVS = 723,
/// `734 <nick> <limit> <targets> :Monitor list is full.` (Source: RFC2812)
ERR_MONLISTFULL = 734,
/// `764 <target> :metadata limit reached` (Source: RFC2812)
ERR_METADATALIMIT = 764,
/// `765 <target> :invalid metadata target` (Source: RFC2812)
ERR_TARGETINVALID = 765,
/// `766 <key> :no matching key` (Source: RFC2812)
ERR_NOMATCHINGKEY = 766,
/// `767 <key> :invalid metadata key` (Source: RFC2812)
ERR_KEYINVALID = 767,
/// `768 <target> <key> :key not set` (Source: RFC2812)
ERR_KEYNOTSET = 768,
/// `769 <target> <key> :permission denied` (Source: RFC2812)
ERR_KEYNOPERMISSION = 769,
/// `902 <nick> :You must use a nick assigned to you.` (Source: IRCv3)
ERR_NICKLOCKED = 902,
/// `904 <nick> :SASL authentication failed` (Source: IRCv3)
ERR_SASLFAIL = 904,
/// `905 <nick> :SASL message too long` (Source: IRCv3)
ERR_SASLTOOLONG = 905,
/// `906 <nick> :SASL authentication aborted` (Source: IRCv3)
ERR_SASLABORT = 906,
/// `907 <nick> :You have already authenticated using SASL` (Source: IRCv3)
ERR_SASLALREADY = 907
}
impl Response {
/// Determines whether or not this response is an error response.
///
/// This error consideration is according to RFC2812, but is rather simplistic. It considers all
/// response codes above 400 to be errors, which misclassifies some extensions (e.g. from IRCv3)
/// that add responses and errors both in the same range (typically 700s or 900s).
pub fn is_error(&self) -> bool {
*self as u16 >= 400
}
}
impl FromStr for Response {
type Err = &'static str;
fn from_str(s: &str) -> Result<Response, &'static str> {
if let Ok(rc) = s.parse() {
match Response::from_u16(rc) {
Some(r) => Ok(r),
None => Err("Failed to parse due to unknown response code."),
}
} else {
Err("Failed to parse response code.")
}
}
}
#[cfg(test)]
mod test {
use super::Response;
#[test]
fn is_error() {
assert!(!Response::RPL_NAMREPLY.is_error());
assert!(Response::ERR_NICKNAMEINUSE.is_error());
}
}

335
irc/src/client/conn.rs Normal file

@ -0,0 +1,335 @@
//! A module providing IRC connections for use by `IrcServer`s.
use futures_channel::mpsc::UnboundedSender;
use futures_util::{sink::Sink, stream::Stream};
use std::{
fmt,
pin::Pin,
task::{Context, Poll},
};
use tokio::net::TcpStream;
use tokio_util::codec::Decoder;
#[cfg(feature = "proxy")]
use tokio_socks::tcp::Socks5Stream;
#[cfg(feature = "proxy")]
use crate::client::data::ProxyType;
#[cfg(feature = "tls-native")]
use std::{fs::File, io::Read};
#[cfg(feature = "tls-native")]
use native_tls::{Certificate, Identity, TlsConnector};
#[cfg(feature = "tls-native")]
use tokio_tls::{self, TlsStream};
#[cfg(feature = "tls-rust")]
use std::{
fs::File,
io::{BufReader, Error, ErrorKind},
sync::Arc,
};
#[cfg(feature = "tls-rust")]
use webpki_roots::TLS_SERVER_ROOTS;
#[cfg(feature = "tls-rust")]
use tokio_rustls::{
client::TlsStream,
rustls::{internal::pemfile::certs, ClientConfig, PrivateKey},
webpki::DNSNameRef,
TlsConnector,
};
use crate::{
client::{
data::Config,
mock::MockStream,
transport::{LogView, Logged, Transport},
},
error,
proto::{IrcCodec, Message},
};
/// An IRC connection used internally by `IrcServer`.
pub enum Connection {
#[doc(hidden)]
Unsecured(Transport<TcpStream>),
#[doc(hidden)]
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Secured(Transport<TlsStream<TcpStream>>),
#[doc(hidden)]
Mock(Logged<MockStream>),
}
impl fmt::Debug for Connection {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
Connection::Unsecured(_) => "Connection::Unsecured(...)",
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(_) => "Connection::Secured(...)",
Connection::Mock(_) => "Connection::Mock(...)",
}
)
}
}
impl Connection {
/// Creates a new `Connection` using the specified `Config`
pub(crate) async fn new(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Connection> {
if config.use_mock_connection() {
log::info!("Connecting via mock to {}.", config.server()?);
return Ok(Connection::Mock(Logged::wrap(
Self::new_mocked_transport(config, tx).await?,
)));
}
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
{
if config.use_tls() {
log::info!("Connecting via TLS to {}.", config.server()?);
return Ok(Connection::Secured(
Self::new_secured_transport(config, tx).await?,
));
}
}
log::info!("Connecting to {}.", config.server()?);
Ok(Connection::Unsecured(
Self::new_unsecured_transport(config, tx).await?,
))
}
#[cfg(not(feature = "proxy"))]
async fn new_stream(config: &Config) -> error::Result<TcpStream> {
Ok(TcpStream::connect((config.server()?, config.port())).await?)
}
#[cfg(feature = "proxy")]
async fn new_stream(config: &Config) -> error::Result<TcpStream> {
let server = config.server()?;
let port = config.port();
let address = (server, port);
match config.proxy_type() {
ProxyType::None => Ok(TcpStream::connect(address).await?),
ProxyType::Socks5 => {
let proxy_server = config.proxy_server();
let proxy_port = config.proxy_port();
let proxy = (proxy_server, proxy_port);
log::info!("Setup proxy {:?}.", proxy);
let proxy_username = config.proxy_username();
let proxy_password = config.proxy_password();
if !proxy_username.is_empty() || !proxy_password.is_empty() {
return Ok(Socks5Stream::connect_with_password(
proxy,
address,
proxy_username,
proxy_password,
)
.await?
.into_inner());
}
Ok(Socks5Stream::connect(proxy, address).await?.into_inner())
}
}
}
async fn new_unsecured_transport(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Transport<TcpStream>> {
let stream = Self::new_stream(config).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
Ok(Transport::new(&config, framed, tx))
}
#[cfg(feature = "tls-native")]
async fn new_secured_transport(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Transport<TlsStream<TcpStream>>> {
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);
log::info!("Added {} to trusted certificates.", cert_path);
}
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);
log::info!(
"Using {} for client certificate authentication.",
client_cert_path
);
}
if config.insecure() {
builder.danger_accept_invalid_certs(true);
}
let connector: tokio_tls::TlsConnector = builder.build()?.into();
let domain = config.server()?;
let stream = Self::new_stream(config).await?;
let stream = connector.connect(domain, stream).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
Ok(Transport::new(&config, framed, tx))
}
#[cfg(feature = "tls-rust")]
async fn new_secured_transport(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Transport<TlsStream<TcpStream>>> {
let mut builder = ClientConfig::default();
builder
.root_store
.add_server_trust_anchors(&TLS_SERVER_ROOTS);
if let Some(cert_path) = config.cert_path() {
let file = File::open(cert_path)?;
let mut cert_data = BufReader::new(file);
builder
.root_store
.add_pem_file(&mut cert_data)
.map_err(|_| {
error::Error::Io(Error::new(ErrorKind::InvalidInput, "invalid cert"))
})?;
log::info!("Added {} to trusted certificates.", cert_path);
}
if let Some(client_cert_path) = config.client_cert_path() {
let client_cert_pass = PrivateKey(Vec::from(config.client_cert_pass()));
let file = File::open(client_cert_path)?;
let client_cert_data = certs(&mut BufReader::new(file)).map_err(|_| {
error::Error::Io(Error::new(ErrorKind::InvalidInput, "invalid cert"))
})?;
builder
.set_single_client_cert(client_cert_data, client_cert_pass)
.map_err(|err| error::Error::Io(Error::new(ErrorKind::InvalidInput, err)))?;
log::info!(
"Using {} for client certificate authentication.",
client_cert_path
);
}
let connector = TlsConnector::from(Arc::new(builder));
let domain = DNSNameRef::try_from_ascii_str(config.server()?)?;
let stream = Self::new_stream(config).await?;
let stream = connector.connect(domain, stream).await?;
let framed = IrcCodec::new(config.encoding())?.framed(stream);
Ok(Transport::new(&config, framed, tx))
}
async fn new_mocked_transport(
config: &Config,
tx: UnboundedSender<Message>,
) -> error::Result<Transport<MockStream>> {
use encoding::{label::encoding_from_whatwg_label, EncoderTrap};
let encoding = encoding_from_whatwg_label(config.encoding()).ok_or_else(|| {
error::Error::UnknownCodec {
codec: config.encoding().to_owned(),
}
})?;
let init_str = config.mock_initial_value();
let initial = encoding
.encode(init_str, EncoderTrap::Replace)
.map_err(|data| error::Error::CodecFailed {
codec: encoding.name(),
data: data.into_owned(),
})?;
let stream = MockStream::new(&initial);
let framed = IrcCodec::new(config.encoding())?.framed(stream);
Ok(Transport::new(&config, framed, tx))
}
/// Gets a view of the internal logging if and only if this connection is using a mock stream.
/// Otherwise, this will always return `None`. This is used for unit testing.
pub fn log_view(&self) -> Option<LogView> {
match *self {
Connection::Mock(ref inner) => Some(inner.view()),
_ => None,
}
}
}
impl Stream for Connection {
type Item = error::Result<Message>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_next(cx),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).poll_next(cx),
Connection::Mock(inner) => Pin::new(inner).poll_next(cx),
}
}
}
impl Sink<Message> for Connection {
type Error = error::Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_ready(cx),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).poll_ready(cx),
Connection::Mock(inner) => Pin::new(inner).poll_ready(cx),
}
}
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).start_send(item),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).start_send(item),
Connection::Mock(inner) => Pin::new(inner).start_send(item),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_flush(cx),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).poll_flush(cx),
Connection::Mock(inner) => Pin::new(inner).poll_flush(cx),
}
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match &mut *self {
Connection::Unsecured(inner) => Pin::new(inner).poll_close(cx),
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
Connection::Secured(inner) => Pin::new(inner).poll_close(cx),
Connection::Mock(inner) => Pin::new(inner).poll_close(cx),
}
}
}

@ -0,0 +1,18 @@
{
"owners": [
"test"
],
"nickname": "test",
"username": "test",
"realname": "test",
"password": "",
"server": "irc.test.net",
"port": 6667,
"encoding": "UTF-8",
"channels": [
"#test",
"#test2"
],
"umodes": "+BR",
"options": {}
}

@ -0,0 +1,12 @@
owners = ["test"]
nickname = "test"
username = "test"
realname = "test"
server = "irc.test.net"
port = 6667
password = ""
encoding = "UTF-8"
channels = ["#test", "#test2"]
umodes = "+BR"
[options]

@ -0,0 +1,15 @@
---
owners:
- test
nickname: test
username: test
realname: test
server: irc.test.net
port: 6667
password: ""
encoding: UTF-8
channels:
- "#test"
- "#test2"
umodes: +BR
options: {}

@ -0,0 +1,727 @@
//! JSON configuration files using serde
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::File,
io::prelude::*,
path::{Path, PathBuf},
};
#[cfg(feature = "json_config")]
use serde_json;
#[cfg(feature = "yaml_config")]
use serde_yaml;
#[cfg(feature = "toml_config")]
use toml;
#[cfg(feature = "proxy")]
use crate::client::data::proxy::ProxyType;
use crate::error::Error::InvalidConfig;
#[cfg(feature = "toml_config")]
use crate::error::TomlError;
use crate::error::{ConfigError, Result};
/// 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, Default, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Config {
/// A list of the owners of the client by nickname (for bots).
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
#[cfg_attr(feature = "serde", serde(default))]
pub owners: Vec<String>,
/// The client's nickname.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub nickname: Option<String>,
/// The client's NICKSERV password.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub nick_password: Option<String>,
/// Alternative nicknames for the client, if the default is taken.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
#[cfg_attr(feature = "serde", serde(default))]
pub alt_nicks: Vec<String>,
/// The client's username.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub username: Option<String>,
/// The client's real name.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub realname: Option<String>,
/// The server to connect to.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub server: Option<String>,
/// The port to connect on.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub port: Option<u16>,
/// The password to connect to the server.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub password: Option<String>,
/// The proxy type to connect to.
#[cfg(feature = "proxy")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub proxy_type: Option<ProxyType>,
/// The proxy server to connect to.
#[cfg(feature = "proxy")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub proxy_server: Option<String>,
/// The proxy port to connect on.
#[cfg(feature = "proxy")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub proxy_port: Option<u16>,
/// The username to connect to the proxy server.
#[cfg(feature = "proxy")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub proxy_username: Option<String>,
/// The password to connect to the proxy server.
#[cfg(feature = "proxy")]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub proxy_password: Option<String>,
/// Whether or not to use TLS.
/// Clients will automatically panic if this is enabled without TLS support.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub use_tls: Option<bool>,
/// The path to the TLS certificate for this server in DER format.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub cert_path: Option<String>,
/// The path to a TLS certificate to use for CertFP client authentication in DER format.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub client_cert_path: Option<String>,
/// The password for the certificate to use in CertFP authentication.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub client_cert_pass: Option<String>,
/// Whether or not to do danger_accept_invalid_certs().
pub insecure: Option<bool>,
/// The encoding type used for this connection.
/// This is typically UTF-8, but could be something else.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub encoding: Option<String>,
/// A list of channels to join on connection.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty"))]
#[cfg_attr(feature = "serde", serde(default))]
pub channels: Vec<String>,
/// User modes to set on connect. Example: "+RB -x"
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub umodes: Option<String>,
/// The text that'll be sent in response to CTCP USERINFO requests.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_info: Option<String>,
/// The text that'll be sent in response to CTCP VERSION requests.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub version: Option<String>,
/// The text that'll be sent in response to CTCP SOURCE requests.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub source: Option<String>,
/// The amount of inactivity in seconds before the client will ping the server.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub ping_time: Option<u32>,
/// The amount of time in seconds for a client to reconnect due to no ping response.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub ping_timeout: Option<u32>,
/// The length in seconds of a rolling window for message throttling. If more than
/// `max_messages_in_burst` messages are sent within `burst_window_length` seconds, additional
/// messages will be delayed automatically as appropriate. In particular, in the past
/// `burst_window_length` seconds, there will never be more than `max_messages_in_burst` messages
/// sent.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub burst_window_length: Option<u32>,
/// The maximum number of messages that can be sent in a burst window before they'll be delayed.
/// Messages are automatically delayed as appropriate.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub max_messages_in_burst: Option<u32>,
/// Whether the client should use NickServ GHOST to reclaim its primary nickname if it is in
/// use. This has no effect if `nick_password` is not set.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
#[cfg_attr(feature = "serde", serde(default))]
pub should_ghost: bool,
/// The command(s) that should be sent to NickServ to recover a nickname. The nickname and
/// password will be appended in that order after the command.
/// E.g. `["RECOVER", "RELEASE"]` means `RECOVER nick pass` and `RELEASE nick pass` will be sent
/// in that order.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg_attr(feature = "serde", serde(default))]
pub ghost_sequence: Option<Vec<String>>,
/// Whether or not to use a fake connection for testing purposes. You probably will never want
/// to enable this, but it is used in unit testing for the `irc` crate.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_false"))]
#[cfg_attr(feature = "serde", serde(default))]
pub use_mock_connection: bool,
/// The initial value used by the fake connection for testing. You probably will never need to
/// set this, but it is used in unit testing for the `irc` crate.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub mock_initial_value: Option<String>,
/// A mapping of channel names to keys for join-on-connect.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))]
#[cfg_attr(feature = "serde", serde(default))]
pub channel_keys: HashMap<String, String>,
/// A map of additional options to be stored in config.
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))]
#[cfg_attr(feature = "serde", serde(default))]
pub options: HashMap<String, String>,
/// The path that this configuration was loaded from.
///
/// This should not be specified in any configuration. It will automatically be handled by the library.
#[cfg_attr(feature = "serde", serde(skip_serializing))]
#[doc(hidden)]
pub path: Option<PathBuf>,
}
#[cfg(feature = "serde")]
fn is_false(v: &bool) -> bool {
!v
}
impl Config {
fn with_path<P: AsRef<Path>>(mut self, path: P) -> Config {
self.path = Some(path.as_ref().to_owned());
self
}
fn path(&self) -> String {
self.path
.as_ref()
.map(|buf| buf.to_string_lossy().into_owned())
.unwrap_or_else(|| "<none>".to_owned())
}
/// Loads a configuration from the desired path. This will use the file extension to detect
/// which format to parse the file as (json, toml, or yaml). Using each format requires having
/// its respective crate feature enabled. Only json is available by default.
pub fn load<P: AsRef<Path>>(path: P) -> Result<Config> {
let mut file = File::open(&path)?;
let mut data = String::new();
file.read_to_string(&mut data)?;
let res = match path.as_ref().extension().and_then(|s| s.to_str()) {
Some("json") => Config::load_json(&path, &data),
Some("toml") => Config::load_toml(&path, &data),
Some("yaml") | Some("yml") => Config::load_yaml(&path, &data),
Some(ext) => Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::UnknownConfigFormat {
format: ext.to_owned(),
},
}),
None => Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::MissingExtension,
}),
};
res.map(|config| config.with_path(path))
}
#[cfg(feature = "json_config")]
fn load_json<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
serde_json::from_str(data).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidJson(e),
})
}
#[cfg(not(feature = "json_config"))]
fn load_json<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
})
}
#[cfg(feature = "toml_config")]
fn load_toml<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
toml::from_str(data).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidToml(TomlError::Read(e)),
})
}
#[cfg(not(feature = "toml_config"))]
fn load_toml<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
})
}
#[cfg(feature = "yaml_config")]
fn load_yaml<P: AsRef<Path>>(path: P, data: &str) -> Result<Config> {
serde_yaml::from_str(data).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidYaml(e),
})
}
#[cfg(not(feature = "yaml_config"))]
fn load_yaml<P: AsRef<Path>>(path: P, _: &str) -> Result<Config> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
})
}
/// Saves a configuration to the desired path. This will use the file extension to detect
/// which format to parse the file as (json, toml, or yaml). Using each format requires having
/// its respective crate feature enabled. Only json is available by default.
pub fn save<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let _ = self.path.take();
let mut file = File::create(&path)?;
let data = match path.as_ref().extension().and_then(|s| s.to_str()) {
Some("json") => self.save_json(&path)?,
Some("toml") => self.save_toml(&path)?,
Some("yaml") | Some("yml") => self.save_yaml(&path)?,
Some(ext) => {
return Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::UnknownConfigFormat {
format: ext.to_owned(),
},
})
}
None => {
return Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::MissingExtension,
})
}
};
file.write_all(data.as_bytes())?;
self.path = Some(path.as_ref().to_owned());
Ok(())
}
#[cfg(feature = "json_config")]
fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
serde_json::to_string(self).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidJson(e),
})
}
#[cfg(not(feature = "json_config"))]
fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
})
}
#[cfg(feature = "toml_config")]
fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
toml::to_string(self).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidToml(TomlError::Write(e)),
})
}
#[cfg(not(feature = "toml_config"))]
fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
})
}
#[cfg(feature = "yaml_config")]
fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
serde_yaml::to_string(self).map_err(|e| InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::InvalidYaml(e),
})
}
#[cfg(not(feature = "yaml_config"))]
fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
Err(InvalidConfig {
path: path.as_ref().to_string_lossy().into_owned(),
cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
})
}
/// Determines whether or not the nickname provided is the owner of the bot.
pub fn is_owner(&self, nickname: &str) -> bool {
self.owners.iter().find(|n| *n == nickname).is_some()
}
/// Gets the nickname specified in the configuration.
pub fn nickname(&self) -> Result<&str> {
self.nickname
.as_ref()
.map(String::as_str)
.ok_or_else(|| InvalidConfig {
path: self.path(),
cause: ConfigError::NicknameNotSpecified,
})
}
/// Gets the bot's nickserv password specified in the configuration.
/// This defaults to an empty string when not specified.
pub fn nick_password(&self) -> &str {
self.nick_password.as_ref().map_or("", String::as_str)
}
/// Gets the alternate nicknames specified in the configuration.
/// This defaults to an empty vector when not specified.
pub fn alternate_nicknames(&self) -> &[String] {
&self.alt_nicks
}
/// Gets the username specified in the configuration.
/// This defaults to the user's nickname when not specified.
pub fn username(&self) -> &str {
self.username
.as_ref()
.map_or(self.nickname().unwrap_or("user"), |s| &s)
}
/// Gets the real name specified in the configuration.
/// This defaults to the user's nickname when not specified.
pub fn real_name(&self) -> &str {
self.realname
.as_ref()
.map_or(self.nickname().unwrap_or("irc"), |s| &s)
}
/// Gets the address of the server specified in the configuration.
pub fn server(&self) -> Result<&str> {
self.server
.as_ref()
.map(String::as_str)
.ok_or_else(|| InvalidConfig {
path: self.path(),
cause: ConfigError::ServerNotSpecified,
})
}
/// Gets the port of the server specified in the configuration.
/// This defaults to 6697 (or 6667 if use_tls is specified as false) when not specified.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn port(&self) -> u16 {
self.port.as_ref().cloned().unwrap_or(match self.use_tls() {
true => 6697,
false => 6667,
})
}
/// Gets the port of the server specified in the configuration.
/// This defaults to 6667 when not specified.
#[cfg(not(any(feature = "tls-native", feature = "tls-rust")))]
pub fn port(&self) -> u16 {
self.port.as_ref().cloned().unwrap_or(6667)
}
/// Gets the server password specified in the configuration.
/// This defaults to an empty string when not specified.
pub fn password(&self) -> &str {
self.password.as_ref().map_or("", String::as_str)
}
/// Gets the type of the proxy specified in the configuration.
/// This defaults to a None ProxyType when not specified.
#[cfg(feature = "proxy")]
pub fn proxy_type(&self) -> ProxyType {
self.proxy_type.as_ref().cloned().unwrap_or(ProxyType::None)
}
/// Gets the address of the proxy specified in the configuration.
/// This defaults to "localhost" string when not specified.
#[cfg(feature = "proxy")]
pub fn proxy_server(&self) -> &str {
self.proxy_server
.as_ref()
.map_or("localhost", String::as_str)
}
/// Gets the port of the proxy specified in the configuration.
/// This defaults to 1080 when not specified.
#[cfg(feature = "proxy")]
pub fn proxy_port(&self) -> u16 {
self.proxy_port.as_ref().cloned().unwrap_or(1080)
}
/// Gets the username of the proxy specified in the configuration.
/// This defaults to an empty string when not specified.
#[cfg(feature = "proxy")]
pub fn proxy_username(&self) -> &str {
self.proxy_username.as_ref().map_or("", String::as_str)
}
/// Gets the password of the proxy specified in the configuration.
/// This defaults to an empty string when not specified.
#[cfg(feature = "proxy")]
pub fn proxy_password(&self) -> &str {
self.proxy_password.as_ref().map_or("", String::as_str)
}
/// Gets whether or not to use TLS with this connection.
/// This defaults to true when not specified.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn use_tls(&self) -> bool {
self.use_tls.as_ref().cloned().map_or(true, |s| s)
}
/// Gets the path to the TLS certificate in DER format if specified.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn cert_path(&self) -> Option<&str> {
self.cert_path.as_ref().map(String::as_str)
}
/// Gets whether or not to do danger_accept_invalid_certs().
/// This defaults to false when not specified.
pub fn insecure(&self) -> bool {
self.insecure.as_ref().cloned().unwrap_or(false)
}
/// Gets the path to the client authentication certificate in DER format if specified.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn client_cert_path(&self) -> Option<&str> {
self.client_cert_path.as_ref().map(String::as_str)
}
/// Gets the password to the client authentication certificate.
#[cfg(any(feature = "tls-native", feature = "tls-rust"))]
pub fn client_cert_pass(&self) -> &str {
self.client_cert_pass.as_ref().map_or("", String::as_str)
}
/// 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 {
self.encoding.as_ref().map_or("UTF-8", |s| &s)
}
/// Gets the channels to join upon connection.
/// This defaults to an empty vector if it's not specified.
pub fn channels(&self) -> &[String] {
&self.channels
}
/// Gets the key for the specified channel if it exists in the configuration.
pub fn channel_key(&self, chan: &str) -> Option<&str> {
self.channel_keys.get(chan).map(String::as_str)
}
/// Gets the user modes to set on connect specified in the configuration.
/// This defaults to an empty string when not specified.
pub fn umodes(&self) -> &str {
self.umodes.as_ref().map_or("", String::as_str)
}
/// Gets the string to be sent in response to CTCP USERINFO requests.
/// This defaults to an empty string when not specified.
pub fn user_info(&self) -> &str {
self.user_info.as_ref().map_or("", String::as_str)
}
/// Gets the string to be sent in response to CTCP VERSION requests.
/// This defaults to `irc:version:env` when not specified.
/// For example, `irc:0.12.0:Compiled with rustc`
pub fn version(&self) -> &str {
self.version.as_ref().map_or(crate::VERSION_STR, |s| &s)
}
/// Gets the string to be sent in response to CTCP SOURCE requests.
/// This defaults to `https://github.com/aatxe/irc` when not specified.
pub fn source(&self) -> &str {
self.source
.as_ref()
.map_or("https://github.com/aatxe/irc", String::as_str)
}
/// Gets the amount of time in seconds for the interval at which the client pings the server.
/// This defaults to 180 seconds when not specified.
pub fn ping_time(&self) -> u32 {
self.ping_time.as_ref().cloned().unwrap_or(180)
}
/// Gets the amount of time in seconds for the client to disconnect after not receiving a ping
/// response.
/// This defaults to 10 seconds when not specified.
pub fn ping_timeout(&self) -> u32 {
self.ping_timeout.as_ref().cloned().unwrap_or(10)
}
/// The amount of time in seconds to consider a window for burst messages. The message throttling
/// system maintains the invariant that in the past `burst_window_length` seconds, the maximum
/// number of messages sent is `max_messages_in_burst`.
/// This defaults to 8 seconds when not specified.
pub fn burst_window_length(&self) -> u32 {
self.burst_window_length.as_ref().cloned().unwrap_or(8)
}
/// The maximum number of messages that can be sent in a burst window before they'll be delayed.
/// Messages are automatically delayed until the start of the next window. The message throttling
/// system maintains the invariant that in the past `burst_window_length` seconds, the maximum
/// number of messages sent is `max_messages_in_burst`.
/// This defaults to 15 messages when not specified.
pub fn max_messages_in_burst(&self) -> u32 {
self.max_messages_in_burst.as_ref().cloned().unwrap_or(15)
}
/// Gets whether or not to attempt nickname reclamation using NickServ GHOST.
/// This defaults to false when not specified.
pub fn should_ghost(&self) -> bool {
self.should_ghost
}
/// Gets the NickServ command sequence to recover a nickname.
/// This defaults to `["GHOST"]` when not specified.
pub fn ghost_sequence(&self) -> Option<&[String]> {
self.ghost_sequence.as_ref().map(Vec::as_slice)
}
/// Looks up the specified string in the options map.
pub fn get_option(&self, option: &str) -> Option<&str> {
self.options.get(option).map(String::as_str)
}
/// Gets whether or not to use a mock connection for testing.
/// This defaults to false when not specified.
pub fn use_mock_connection(&self) -> bool {
self.use_mock_connection
}
/// Gets the initial value for the mock connection.
/// This defaults to false when not specified.
/// This has no effect if `use_mock_connection` is not `true`.
pub fn mock_initial_value(&self) -> &str {
self.mock_initial_value.as_ref().map_or("", |s| &s)
}
}
#[cfg(test)]
mod test {
use super::Config;
use std::collections::HashMap;
#[cfg(any(
feature = "json_config",
feature = "toml_config",
feature = "yaml_config"
))]
use super::Result;
#[allow(unused)]
fn test_config() -> Config {
Config {
owners: vec![format!("test")],
nickname: Some(format!("test")),
username: Some(format!("test")),
realname: Some(format!("test")),
password: Some(String::new()),
umodes: Some(format!("+BR")),
server: Some(format!("irc.test.net")),
port: Some(6667),
encoding: Some(format!("UTF-8")),
channels: vec![format!("#test"), format!("#test2")],
..Default::default()
}
}
#[test]
fn is_owner() {
let cfg = Config {
owners: vec![format!("test"), format!("test2")],
..Default::default()
};
assert!(cfg.is_owner("test"));
assert!(cfg.is_owner("test2"));
assert!(!cfg.is_owner("test3"));
}
#[test]
fn get_option() {
let cfg = Config {
options: {
let mut map = HashMap::new();
map.insert(format!("testing"), format!("test"));
map
},
..Default::default()
};
assert_eq!(cfg.get_option("testing"), Some("test"));
assert_eq!(cfg.get_option("not"), None);
}
#[test]
#[cfg(feature = "json_config")]
fn load_from_json() -> Result<()> {
const DATA: &str = include_str!("client_config.json");
assert_eq!(
Config::load_json("client_config.json", DATA)?.with_path("client_config.json"),
test_config().with_path("client_config.json")
);
Ok(())
}
#[test]
#[cfg(feature = "toml_config")]
fn load_from_toml() -> Result<()> {
const DATA: &str = include_str!("client_config.toml");
assert_eq!(
Config::load_toml("client_config.toml", DATA)?.with_path("client_config.toml"),
test_config().with_path("client_config.toml")
);
Ok(())
}
#[test]
#[cfg(feature = "yaml_config")]
fn load_from_yaml() -> Result<()> {
const DATA: &str = include_str!("client_config.yaml");
assert_eq!(
Config::load_yaml("client_config.yaml", DATA)?.with_path("client_config.yaml"),
test_config().with_path("client_config.yaml")
);
Ok(())
}
}

@ -0,0 +1,11 @@
//! Data related to IRC functionality.
pub use crate::client::data::config::Config;
#[cfg(feature = "proxy")]
pub use crate::client::data::proxy::ProxyType;
pub use crate::client::data::user::{AccessLevel, User};
pub mod config;
#[cfg(feature = "proxy")]
pub mod proxy;
pub mod user;

@ -0,0 +1,33 @@
//! A feature which allow us to connect to IRC via a proxy.
//!
//! ```
//! use irc::client::prelude::Config;
//! use irc::client::data::ProxyType;
//!
//! # fn main() {
//! let config = Config {
//! nickname: Some("test".to_owned()),
//! server: Some("irc.example.com".to_owned()),
//! proxy_type: Some(ProxyType::Socks5),
//! proxy_server: Some("127.0.0.1".to_owned()),
//! proxy_port: Some(9050),
//! ..Config::default()
//! };
//! # }
//! ```
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// An enum which defines which type of proxy should be in use.
#[cfg(feature = "proxy")]
#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum ProxyType {
/// Does not use any proxy.
None,
/// Use a SOCKS5 proxy.
/// DNS queries are also sent via the proxy.
Socks5,
}

355
irc/src/client/data/user.rs Normal file

@ -0,0 +1,355 @@
//! Data for tracking user information.
use std::borrow::ToOwned;
use std::cmp::Ordering;
use std::cmp::Ordering::{Equal, Greater, Less};
use std::str::FromStr;
use crate::proto::{ChannelMode, Mode};
/// IRC User data.
#[derive(Clone, Debug)]
pub struct User {
/// The user's nickname.
nickname: String,
/// The user's username.
username: Option<String>,
/// The user's hostname.
hostname: Option<String>,
/// The user's highest access level.
highest_access_level: AccessLevel,
/// All of the user's current access levels.
access_levels: Vec<AccessLevel>,
}
impl User {
/// Creates a new User.
pub fn new(string: &str) -> User {
let ranks: Vec<_> = AccessLevelIterator::new(string).collect();
let mut state = &string[ranks.len()..];
let nickname = state.find('!').map_or(state, |i| &state[..i]).to_owned();
state = state.find('!').map_or("", |i| &state[i + 1..]);
let username = state.find('@').map(|i| state[..i].to_owned());
let hostname = state.find('@').map(|i| state[i + 1..].to_owned());
User {
nickname: nickname,
username: username,
hostname: hostname,
access_levels: {
let mut ranks = ranks.clone();
ranks.push(AccessLevel::Member);
ranks
},
highest_access_level: {
let mut max = AccessLevel::Member;
for rank in ranks {
if rank > max {
max = rank
}
}
max
},
}
}
/// Gets the nickname of the user.
pub fn get_nickname(&self) -> &str {
&self.nickname
}
/// Gets the username of the user, if it's known.
/// This requires the IRCv3.2 extension `userhost-in-name`.
pub fn get_username(&self) -> Option<&str> {
self.username.as_ref().map(|s| &s[..])
}
/// Gets the hostname of the user, if it's known.
/// This requires the IRCv3.2 extension `userhost-in-name`.
pub fn get_hostname(&self) -> Option<&str> {
self.hostname.as_ref().map(|s| &s[..])
}
/// Gets the user's highest access level.
pub fn highest_access_level(&self) -> AccessLevel {
self.highest_access_level
}
/// Gets all the user's access levels.
pub fn access_levels(&self) -> Vec<AccessLevel> {
self.access_levels.clone()
}
/// Updates the user's access level.
pub fn update_access_level(&mut self, mode: &Mode<ChannelMode>) {
match *mode {
Mode::Plus(ChannelMode::Founder, _) => self.add_access_level(AccessLevel::Owner),
Mode::Minus(ChannelMode::Founder, _) => self.sub_access_level(AccessLevel::Owner),
Mode::Plus(ChannelMode::Admin, _) => self.add_access_level(AccessLevel::Admin),
Mode::Minus(ChannelMode::Admin, _) => self.sub_access_level(AccessLevel::Admin),
Mode::Plus(ChannelMode::Oper, _) => self.add_access_level(AccessLevel::Oper),
Mode::Minus(ChannelMode::Oper, _) => self.sub_access_level(AccessLevel::Oper),
Mode::Plus(ChannelMode::Halfop, _) => self.add_access_level(AccessLevel::HalfOp),
Mode::Minus(ChannelMode::Halfop, _) => self.sub_access_level(AccessLevel::HalfOp),
Mode::Plus(ChannelMode::Voice, _) => self.add_access_level(AccessLevel::Voice),
Mode::Minus(ChannelMode::Voice, _) => self.sub_access_level(AccessLevel::Voice),
_ => {}
}
}
/// Adds an access level to the list, and updates the highest level if necessary.
fn add_access_level(&mut self, level: AccessLevel) {
if level > self.highest_access_level() {
self.highest_access_level = level
}
self.access_levels.push(level)
}
/// Removes an access level from the list, and updates the highest level if necessary.
fn sub_access_level(&mut self, level: AccessLevel) {
if let Some(n) = self.access_levels.iter().position(|x| *x == level) {
self.access_levels.swap_remove(n);
}
if level == self.highest_access_level() {
self.highest_access_level = {
let mut max = AccessLevel::Member;
for level in &self.access_levels {
if level > &max {
max = *level
}
}
max
}
}
}
}
impl PartialEq for User {
fn eq(&self, other: &User) -> bool {
self.nickname == other.nickname
&& self.username == other.username
&& self.hostname == other.hostname
}
}
/// The user's access level.
#[derive(Copy, PartialEq, Clone, Debug)]
pub enum AccessLevel {
/// The channel owner (~).
Owner,
/// A channel administrator (&).
Admin,
/// A channel operator (@),
Oper,
/// A channel half-oper (%),
HalfOp,
/// A user with voice (+),
Voice,
/// A normal user,
Member,
}
impl PartialOrd for AccessLevel {
fn partial_cmp(&self, other: &AccessLevel) -> Option<Ordering> {
if self == other {
return Some(Equal);
}
match *self {
AccessLevel::Owner => Some(Greater),
AccessLevel::Admin => {
if other == &AccessLevel::Owner {
Some(Less)
} else {
Some(Greater)
}
}
AccessLevel::Oper => {
if other == &AccessLevel::Owner || other == &AccessLevel::Admin {
Some(Less)
} else {
Some(Greater)
}
}
AccessLevel::HalfOp => {
if other == &AccessLevel::Voice || other == &AccessLevel::Member {
Some(Greater)
} else {
Some(Less)
}
}
AccessLevel::Voice => {
if other == &AccessLevel::Member {
Some(Greater)
} else {
Some(Less)
}
}
AccessLevel::Member => Some(Less),
}
}
}
impl FromStr for AccessLevel {
type Err = &'static str;
fn from_str(s: &str) -> Result<AccessLevel, &'static str> {
match s.chars().next() {
Some('~') => Ok(AccessLevel::Owner),
Some('&') => Ok(AccessLevel::Admin),
Some('@') => Ok(AccessLevel::Oper),
Some('%') => Ok(AccessLevel::HalfOp),
Some('+') => Ok(AccessLevel::Voice),
None => Err("No access level in an empty string."),
_ => Err("Failed to parse access level."),
}
}
}
/// An iterator used to parse access levels from strings.
struct AccessLevelIterator {
value: String,
}
impl AccessLevelIterator {
pub fn new(value: &str) -> AccessLevelIterator {
AccessLevelIterator {
value: value.to_owned(),
}
}
}
impl Iterator for AccessLevelIterator {
type Item = AccessLevel;
fn next(&mut self) -> Option<AccessLevel> {
let ret = self.value.parse();
if !self.value.is_empty() {
self.value = self.value.chars().skip(1).collect()
}
ret.ok()
}
}
#[cfg(test)]
mod test {
use super::AccessLevel::*;
use super::{AccessLevel, User};
use crate::proto::ChannelMode as M;
use crate::proto::Mode::*;
#[test]
fn parse_access_level() {
assert!("member".parse::<AccessLevel>().is_err());
assert_eq!("~owner".parse::<AccessLevel>().unwrap(), Owner);
assert_eq!("&admin".parse::<AccessLevel>().unwrap(), Admin);
assert_eq!("@oper".parse::<AccessLevel>().unwrap(), Oper);
assert_eq!("%halfop".parse::<AccessLevel>().unwrap(), HalfOp);
assert_eq!("+voice".parse::<AccessLevel>().unwrap(), Voice);
assert!("".parse::<AccessLevel>().is_err());
}
#[test]
fn create_user() {
let user = User::new("~owner");
let exp = User {
nickname: format!("owner"),
username: None,
hostname: None,
highest_access_level: Owner,
access_levels: vec![Owner, Member],
};
assert_eq!(user, exp);
assert_eq!(user.highest_access_level, exp.highest_access_level);
assert_eq!(user.access_levels, exp.access_levels);
}
#[test]
fn create_user_complex() {
let user = User::new("~&+user");
let exp = User {
nickname: format!("user"),
username: None,
hostname: None,
highest_access_level: Owner,
access_levels: vec![Owner, Admin, Voice, Member],
};
assert_eq!(user, exp);
assert_eq!(user.highest_access_level, exp.highest_access_level);
assert_eq!(user.access_levels, exp.access_levels);
}
#[test]
fn get_nickname() {
let user = User::new("~owner");
assert_eq!(user.get_nickname(), "owner");
}
#[test]
fn get_username() {
let user = User::new("user!username@hostname");
assert_eq!(user.get_username(), Some("username"));
let user = User::new("user");
assert_eq!(user.get_username(), None);
}
#[test]
fn get_hostname() {
let user = User::new("user!username@hostname");
assert_eq!(user.get_hostname(), Some("hostname"));
let user = User::new("user");
assert_eq!(user.get_hostname(), None);
}
#[test]
fn access_level() {
let user = User::new("~owner");
assert_eq!(user.highest_access_level(), Owner);
}
#[test]
fn update_user_rank() {
let mut user = User::new("user");
assert_eq!(user.highest_access_level, Member);
user.update_access_level(&Plus(M::Founder, None));
assert_eq!(user.highest_access_level, Owner);
user.update_access_level(&Minus(M::Founder, None));
assert_eq!(user.highest_access_level, Member);
user.update_access_level(&Plus(M::Admin, None));
assert_eq!(user.highest_access_level, Admin);
user.update_access_level(&Minus(M::Admin, None));
assert_eq!(user.highest_access_level, Member);
user.update_access_level(&Plus(M::Oper, None));
assert_eq!(user.highest_access_level, Oper);
user.update_access_level(&Minus(M::Oper, None));
assert_eq!(user.highest_access_level, Member);
user.update_access_level(&Plus(M::Halfop, None));
assert_eq!(user.highest_access_level, HalfOp);
user.update_access_level(&Minus(M::Halfop, None));
assert_eq!(user.highest_access_level, Member);
user.update_access_level(&Plus(M::Voice, None));
assert_eq!(user.highest_access_level, Voice);
user.update_access_level(&Minus(M::Voice, None));
assert_eq!(user.highest_access_level, Member);
}
#[test]
fn derank_user_in_full() {
let mut user = User::new("~&@%+user");
assert_eq!(user.highest_access_level, Owner);
assert_eq!(
user.access_levels,
vec![Owner, Admin, Oper, HalfOp, Voice, Member]
);
user.update_access_level(&Minus(M::Halfop, None));
assert_eq!(user.highest_access_level, Owner);
assert_eq!(user.access_levels, vec![Owner, Admin, Oper, Member, Voice]);
user.update_access_level(&Minus(M::Founder, None));
assert_eq!(user.highest_access_level, Admin);
assert_eq!(user.access_levels, vec![Voice, Admin, Oper, Member]);
user.update_access_level(&Minus(M::Admin, None));
assert_eq!(user.highest_access_level, Oper);
assert_eq!(user.access_levels, vec![Voice, Member, Oper]);
user.update_access_level(&Minus(M::Oper, None));
assert_eq!(user.highest_access_level, Voice);
assert_eq!(user.access_levels, vec![Voice, Member]);
user.update_access_level(&Minus(M::Voice, None));
assert_eq!(user.highest_access_level, Member);
assert_eq!(user.access_levels, vec![Member]);
}
}

66
irc/src/client/mock.rs Normal file

@ -0,0 +1,66 @@
use std::{
io::{self, Cursor, Read, Write},
pin::Pin,
task::{Context, Poll},
};
use tokio::io::{AsyncRead, AsyncWrite};
/// A fake stream for testing network applications backed by buffers.
#[derive(Clone, Debug)]
pub struct MockStream {
written: Cursor<Vec<u8>>,
received: Cursor<Vec<u8>>,
}
impl MockStream {
/// Creates a new mock stream with nothing to read.
pub fn empty() -> MockStream {
MockStream::new(&[])
}
/// Creates a new mock stream with the specified bytes to read.
pub fn new(initial: &[u8]) -> MockStream {
MockStream {
written: Cursor::new(vec![]),
received: Cursor::new(initial.to_owned()),
}
}
/// Gets a slice of bytes representing the data that has been written.
pub fn written(&self) -> &[u8] {
self.written.get_ref()
}
/// Gets a slice of bytes representing the data that has been received.
pub fn received(&self) -> &[u8] {
self.received.get_ref()
}
}
impl AsyncRead for MockStream {
fn poll_read(
mut self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
Poll::Ready(self.as_mut().received.read(buf))
}
}
impl AsyncWrite for MockStream {
fn poll_write(
mut self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
Poll::Ready(self.as_mut().written.write(buf))
}
fn poll_flush(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
Poll::Ready(self.as_mut().written.flush())
}
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
Poll::Ready(Ok(()))
}
}

1944
irc/src/client/mod.rs Normal file

Різницю між файлами не показано, бо вона завелика Завантажити різницю

33
irc/src/client/prelude.rs Normal file

@ -0,0 +1,33 @@
//! A client-side IRC prelude, re-exporting the complete high-level IRC client API.
//!
//! # Structure
//! A connection to an IRC server is created via an `IrcClient` which is configured using a
//! `Config` struct that defines data such as which server to connect to, on what port, and
//! using what nickname. The `Client` trait provides an API for actually interacting with the
//! server once a connection has been established. This API intentionally offers only a single
//! method to send `Commands` because it makes it easy to see the whole set of possible
//! interactions with a server. The `ClientExt` trait addresses this deficiency by defining a
//! number of methods that provide a more clear and succinct interface for sending various
//! common IRC commands to the server. An `IrcReactor` can be used to create and manage multiple
//! `IrcClients` with more fine-grained control over error management.
//!
//! The various `proto` types capture details of the IRC protocol that are used throughout the
//! client API. `Message`, `Command`, and `Response` are used to send and receive messages along
//! the connection, and are naturally at the heart of communication in the IRC protocol.
//! `Capability` and `NegotiationVersion` are used to determine (with the server) what IRCv3
//! functionality to enable for the connection. Certain parts of the API offer suggestions for
//! extensions that will improve the user experience, and give examples of how to enable them
//! using `Capability`. `Mode`, `ChannelMode`, and `UserMode` are used in a high-level API for
//! dealing with IRC channel and user modes. They appear in methods for sending mode commands,
//! as well as in the parsed form of received mode commands.
#[cfg(feature = "proxy")]
pub use crate::client::data::ProxyType;
pub use crate::{
client::{data::Config, Client, Sender},
proto::{
Capability, ChannelExt, ChannelMode, Command, Message, Mode, NegotiationVersion, Prefix,
Response, UserMode,
},
};

311
irc/src/client/transport.rs Normal file

@ -0,0 +1,311 @@
//! An IRC transport that wraps an IRC-framed stream to provide a number of features including
//! automatic PING replies, automatic sending of PINGs, and message rate-limiting. This can be used
//! as the basis for implementing a more full IRC client.
use std::{
pin::Pin,
sync::{Arc, RwLock, RwLockReadGuard},
task::{Context, Poll},
time::Duration,
};
use chrono::prelude::*;
use futures_channel::mpsc::UnboundedSender;
use futures_util::{future::Future, ready, sink::Sink, stream::Stream};
use tokio::{
io::{AsyncRead, AsyncWrite},
time::{self, Delay, Interval},
};
use tokio_util::codec::Framed;
use crate::{
client::data::Config,
error,
proto::{Command, IrcCodec, Message},
};
/// Pinger-based futures helper.
struct Pinger {
tx: UnboundedSender<Message>,
/// The amount of time to wait before timing out from no ping response.
ping_timeout: Duration,
/// The instant that the last ping was sent to the server.
ping_deadline: Option<Delay>,
/// The interval at which to send pings.
ping_interval: Interval,
}
impl Pinger {
/// Construct a new pinger helper.
pub fn new(tx: UnboundedSender<Message>, config: &Config) -> Pinger {
let ping_timeout = Duration::from_secs(u64::from(config.ping_timeout()));
Self {
tx,
ping_timeout,
ping_deadline: None,
ping_interval: time::interval(ping_timeout / 2),
}
}
/// Handle an incoming message.
fn handle_message(&mut self, message: &Message) -> error::Result<()> {
match message.command {
// On receiving a `PING` message from the server, we automatically respond with
// the appropriate `PONG` message to keep the connection alive for transport.
Command::PING(ref data, _) => {
self.send_pong(data)?;
}
// Check `PONG` responses from the server. If it matches, we will update the
// last instant that the pong was received. This will prevent timeout.
Command::PONG(_, None) | Command::PONG(_, Some(_)) => {
log::trace!("Received PONG");
self.ping_deadline.take();
}
_ => (),
}
Ok(())
}
/// Send a pong.
fn send_pong(&mut self, data: &str) -> error::Result<()> {
self.tx
.unbounded_send(Command::PONG(data.to_owned(), None).into())?;
Ok(())
}
/// Sends a ping via the transport.
fn send_ping(&mut self) -> error::Result<()> {
log::trace!("Sending PING");
// Creates new ping data using the local timestamp.
let data = format!("{}", Local::now().timestamp());
self.tx
.unbounded_send(Command::PING(data.clone(), None).into())?;
Ok(())
}
/// Set the ping deadline.
fn set_deadline(&mut self) {
if self.ping_deadline.is_none() {
let ping_deadline = time::delay_for(self.ping_timeout);
self.ping_deadline = Some(ping_deadline);
}
}
}
impl Future for Pinger {
type Output = Result<(), error::Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(ping_deadline) = self.as_mut().ping_deadline.as_mut() {
match Pin::new(ping_deadline).poll(cx) {
Poll::Ready(()) => return Poll::Ready(Err(error::Error::PingTimeout)),
Poll::Pending => (),
}
}
if let Poll::Ready(_) = Pin::new(&mut self.as_mut().ping_interval).poll_next(cx) {
self.as_mut().send_ping()?;
self.as_mut().set_deadline();
}
Poll::Pending
}
}
/// An IRC transport that handles core functionality for the IRC protocol. This is used in the
/// implementation of `Connection` and ultimately `IrcServer`, and plays an important role in
/// handling connection timeouts, message throttling, and ping response.
pub struct Transport<T> {
/// The inner connection framed with an `IrcCodec`.
inner: Framed<T, IrcCodec>,
/// Helper for handle pinging.
pinger: Option<Pinger>,
}
impl<T> Unpin for Transport<T> where T: Unpin {}
impl<T> Transport<T>
where
T: Unpin + AsyncRead + AsyncWrite,
{
/// Creates a new `Transport` from the given IRC stream.
pub fn new(
config: &Config,
inner: Framed<T, IrcCodec>,
tx: UnboundedSender<Message>,
) -> Transport<T> {
let pinger = Some(Pinger::new(tx, config));
Transport { inner, pinger }
}
/// Gets the inner stream underlying the `Transport`.
pub fn into_inner(self) -> Framed<T, IrcCodec> {
self.inner
}
}
impl<T> Stream for Transport<T>
where
T: Unpin + AsyncRead + AsyncWrite,
{
type Item = Result<Message, error::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Some(pinger) = self.as_mut().pinger.as_mut() {
match Pin::new(pinger).poll(cx) {
Poll::Ready(result) => result?,
Poll::Pending => (),
}
}
let result = ready!(Pin::new(&mut self.as_mut().inner).poll_next(cx));
let message = match result {
None => return Poll::Ready(None),
Some(message) => message?,
};
if let Some(pinger) = self.as_mut().pinger.as_mut() {
pinger.handle_message(&message)?;
}
Poll::Ready(Some(Ok(message)))
}
}
impl<T> Sink<Message> for Transport<T>
where
T: Unpin + AsyncRead + AsyncWrite,
{
type Error = error::Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(Pin::new(&mut self.as_mut().inner).poll_ready(cx))?;
Poll::Ready(Ok(()))
}
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
log::trace!("[SEND] {}", item);
Pin::new(&mut self.as_mut().inner).start_send(item)?;
Ok(())
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(Pin::new(&mut self.as_mut().inner).poll_flush(cx))?;
Poll::Ready(Ok(()))
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(Pin::new(&mut self.as_mut().inner).poll_close(cx))?;
Poll::Ready(Ok(()))
}
}
/// A view of the logs from a particular `Logged` transport.
#[derive(Clone, Debug)]
pub struct LogView {
sent: Arc<RwLock<Vec<Message>>>,
received: Arc<RwLock<Vec<Message>>>,
}
impl LogView {
/// Gets a read guard for all the messages sent on the transport.
pub fn sent(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> {
self.sent.read().map_err(|_| error::Error::PoisonedLog)
}
/// Gets a read guard for all the messages received on the transport.
pub fn received(&self) -> error::Result<RwLockReadGuard<Vec<Message>>> {
self.received.read().map_err(|_| error::Error::PoisonedLog)
}
}
/// A logged version of the `Transport` that records all sent and received messages.
/// Note: this will introduce some performance overhead by cloning all messages.
pub struct Logged<T> {
inner: Transport<T>,
view: LogView,
}
impl<T> Unpin for Logged<T> where T: Unpin {}
impl<T> Logged<T>
where
T: AsyncRead + AsyncWrite,
{
/// Wraps the given `Transport` in logging.
pub fn wrap(inner: Transport<T>) -> Logged<T> {
Logged {
inner,
view: LogView {
sent: Arc::new(RwLock::new(vec![])),
received: Arc::new(RwLock::new(vec![])),
},
}
}
/// Gets a view of the logging for this transport.
pub fn view(&self) -> LogView {
self.view.clone()
}
}
impl<T> Stream for Logged<T>
where
T: Unpin + AsyncRead + AsyncWrite,
{
type Item = Result<Message, error::Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match ready!(Pin::new(&mut self.as_mut().inner).poll_next(cx)) {
Some(msg) => {
let msg = msg?;
self.view
.received
.write()
.map_err(|_| error::Error::PoisonedLog)?
.push(msg.clone());
Poll::Ready(Some(Ok(msg)))
}
None => Poll::Ready(None),
}
}
}
impl<T> Sink<Message> for Logged<T>
where
T: Unpin + AsyncRead + AsyncWrite,
{
type Error = error::Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.as_mut().inner).poll_ready(cx)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.as_mut().inner).poll_close(cx)
}
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
Pin::new(&mut self.as_mut().inner).start_send(item.clone())?;
self.view
.sent
.write()
.map_err(|_| error::Error::PoisonedLog)?
.push(item);
Ok(())
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.as_mut().inner).poll_flush(cx)
}
}

224
irc/src/error.rs Normal file

@ -0,0 +1,224 @@
//! Errors for `irc` crate using `failure`.
use std::io::Error as IoError;
use std::sync::mpsc::RecvError;
use futures_channel::{
mpsc::{SendError, TrySendError},
oneshot::Canceled,
};
use thiserror::Error;
#[cfg(feature = "tls-rust")]
use tokio_rustls::webpki::InvalidDNSNameError;
use crate::proto::error::{MessageParseError, ProtocolError};
/// A specialized `Result` type for the `irc` crate.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// The main crate-wide error type.
#[derive(Debug, Error)]
pub enum Error {
/// An internal I/O error.
#[error("an io error occurred")]
Io(#[source] IoError),
/// An internal proxy error.
#[cfg(feature = "proxy")]
#[error("a proxy error occurred")]
Proxy(tokio_socks::Error),
/// An internal TLS error.
#[cfg(feature = "tls-native")]
#[error("a TLS error occurred")]
Tls(#[source] native_tls::Error),
/// An internal DNS error.
#[cfg(feature = "tls-rust")]
#[error("a DNS error occurred")]
Dns(#[source] InvalidDNSNameError),
/// An internal synchronous channel closed.
#[error("a sync channel closed")]
SyncChannelClosed(#[source] RecvError),
/// An internal asynchronous channel closed.
#[error("an async channel closed")]
AsyncChannelClosed(#[source] SendError),
/// An internal oneshot channel closed.
#[error("a oneshot channel closed")]
OneShotCanceled(#[source] Canceled),
/// Error for invalid configurations.
#[error("invalid config: {}", path)]
InvalidConfig {
/// The path to the configuration, or "<none>" if none specified.
path: String,
/// The detailed configuration error.
#[source]
cause: ConfigError,
},
/// Error for invalid messages.
#[error("invalid message: {}", string)]
InvalidMessage {
/// The string that failed to parse.
string: String,
/// The detailed message parsing error.
#[source]
cause: MessageParseError,
},
/// Mutex for a logged transport was poisoned making the log inaccessible.
#[error("mutex for a logged transport was poisoned")]
PoisonedLog,
/// Ping timed out due to no response.
#[error("connection reset: no ping response")]
PingTimeout,
/// Failed to lookup an unknown codec.
#[error("unknown codec: {}", codec)]
UnknownCodec {
/// The attempted codec.
codec: String,
},
/// Failed to encode or decode something with the given codec.
#[error("codec {} failed: {}", codec, data)]
CodecFailed {
/// The canonical codec name.
codec: &'static str,
/// The data that failed to encode or decode.
data: String,
},
/// All specified nicknames were in use or unusable.
#[error("none of the specified nicknames were usable")]
NoUsableNick,
/// Stream has already been configured.
#[error("stream has already been configured")]
StreamAlreadyConfigured,
}
/// Errors that occur with configurations.
#[derive(Debug, Error)]
pub enum ConfigError {
/// Failed to parse as TOML.
#[cfg(feature = "toml_config")]
#[error("invalid toml")]
InvalidToml(#[source] TomlError),
/// Failed to parse as JSON.
#[cfg(feature = "json_config")]
#[error("invalid json")]
InvalidJson(#[source] serde_json::Error),
/// Failed to parse as YAML.
#[cfg(feature = "yaml_config")]
#[error("invalid yaml")]
InvalidYaml(#[source] serde_yaml::Error),
/// Failed to parse the given format because it was disabled at compile-time.
#[error("config format disabled: {}", format)]
ConfigFormatDisabled {
/// The disabled file format.
format: &'static str,
},
/// Could not identify the given file format.
#[error("config format unknown: {}", format)]
UnknownConfigFormat {
/// The unknown file extension.
format: String,
},
/// File was missing an extension to identify file format.
#[error("missing format extension")]
MissingExtension,
/// Configuration does not specify a nickname.
#[error("nickname not specified")]
NicknameNotSpecified,
/// Configuration does not specify a server.
#[error("server not specified")]
ServerNotSpecified,
}
/// A wrapper that combines toml's serialization and deserialization errors.
#[cfg(feature = "toml_config")]
#[derive(Debug, Error)]
pub enum TomlError {
/// A TOML deserialization error.
#[error("deserialization failed")]
Read(#[source] toml::de::Error),
/// A TOML serialization error.
#[error("serialization failed")]
Write(#[source] toml::ser::Error),
}
impl From<ProtocolError> for Error {
fn from(e: ProtocolError) -> Error {
match e {
ProtocolError::Io(e) => Error::Io(e),
ProtocolError::InvalidMessage { string, cause } => {
Error::InvalidMessage { string, cause }
}
}
}
}
impl From<IoError> for Error {
fn from(e: IoError) -> Error {
Error::Io(e)
}
}
#[cfg(feature = "proxy")]
impl From<tokio_socks::Error> for Error {
fn from(e: tokio_socks::Error) -> Error {
Error::Proxy(e)
}
}
#[cfg(feature = "tls-native")]
impl From<native_tls::Error> for Error {
fn from(e: native_tls::Error) -> Error {
Error::Tls(e)
}
}
#[cfg(feature = "tls-rust")]
impl From<InvalidDNSNameError> for Error {
fn from(e: InvalidDNSNameError) -> Error {
Error::Dns(e)
}
}
impl From<RecvError> for Error {
fn from(e: RecvError) -> Error {
Error::SyncChannelClosed(e)
}
}
impl From<SendError> for Error {
fn from(e: SendError) -> Error {
Error::AsyncChannelClosed(e)
}
}
impl<T> From<TrySendError<T>> for Error {
fn from(e: TrySendError<T>) -> Error {
Error::AsyncChannelClosed(e.into_send_error())
}
}
impl From<Canceled> for Error {
fn from(e: Canceled) -> Error {
Error::OneShotCanceled(e)
}
}

56
irc/src/lib.rs Normal file

@ -0,0 +1,56 @@
//! A simple, thread-safe, and async-friendly library for IRC clients.
//!
//! # Quick Start
//! The main public API is entirely exported in [`client::prelude`](./client/prelude/index.html).
//! This should include everything necessary to write an IRC client or bot.
//!
//! # A Whirlwind Tour
//! The irc crate is divided into two main modules: [`client`](./client/index.html) and
//! [`proto`](./proto/index.html). As the names suggest, the `client` module captures the whole of
//! the client-side functionality, while the `proto` module features general components of an IRC
//! protocol implementation that could in principle be used in either client or server software.
//! Both modules feature a number of components that are low-level and can be used to build
//! alternative APIs for the IRC protocol. For the average user, the higher-level components for an
//! IRC client are all re-exported in [`client::prelude`](./client/prelude/index.html). That module
//! serves as the best starting point for a new user trying to understand the high-level API.
//!
//! # Example
//!
//! ```no_run
//! use irc::client::prelude::*;
//! use futures::prelude::*;
//!
//! # #[tokio::main]
//! # async fn main() -> irc::error::Result<()> {
//! // configuration is loaded from config.toml into a Config
//! let mut client = Client::new("config.toml").await?;
//! // identify comes from ClientExt
//! client.identify()?;
//!
//! let mut stream = client.stream()?;
//!
//! while let Some(message) = stream.next().await.transpose()? {
//! if let Command::PRIVMSG(channel, message) = message.command {
//! if message.contains(&*client.current_nickname()) {
//! // send_privmsg comes from ClientExt
//! client.send_privmsg(&channel, "beep boop").unwrap();
//! }
//! }
//! }
//! # Ok(())
//! # }
//! ```
#![warn(missing_docs)]
pub extern crate irc_proto as proto;
pub mod client;
pub mod error;
const VERSION_STR: &str = concat!(
env!("CARGO_PKG_NAME"),
":",
env!("CARGO_PKG_VERSION"),
":Compiled with rustc",
);

32
src/main.rs Normal file

@ -0,0 +1,32 @@
use irc::client::prelude::*;
use futures::prelude::*;
use std::time::Duration;
#[tokio::main]
async fn main() -> irc::error::Result<()> {
let config = Config {
nickname: Some("roobot".to_owned()),
server: Some("ircd.chat".to_owned()),
channels: vec!["#sh0rtbus".to_owned()],
port: Some(6697),
use_tls: Some(true),
..Default::default()
};
let mut client = Client::from_config(config).await?;
client.identify()?;
let mut stream = client.stream()?;
let mut interval = tokio::time::interval(Duration::from_secs(60)).fuse();
loop {
futures::select! {
m = stream.select_next_some() => {
println!("{}", m?);
}
_ = interval.select_next_some() => {
client.send_privmsg("#sh0rtbus", ".[d]. ssl+insecure rust bot")?;
}
}
}
}