initial commit
This commit is contained in:
джерело
2e0bf31c2d
коміт
6f1fdf0ec6
|
@ -0,0 +1 @@
|
|||
/target
|
|
@ -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" }
|
|
@ -0,0 +1,2 @@
|
|||
cargo build
|
||||
./target/debug/decoded
|
|
@ -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"]
|
|
@ -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/
|
|
@ -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")?)))
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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..],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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")?;
|
||||
}
|
||||
}
|
|
@ -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")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
Різницю між файлами не показано, бо вона завелика
Завантажити різницю
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -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(()))
|
||||
}
|
||||
}
|
Різницю між файлами не показано, бо вона завелика
Завантажити різницю
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
);
|
|
@ -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")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Завантаження…
Посилання в новій задачі