Port to tokio 0.2
This commit is contained in:
parent
9bb7fa8ba2
commit
549e2e8722
42 changed files with 2361 additions and 2767 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,7 +2,4 @@
|
|||
/irc-proto/target
|
||||
/Cargo.lock
|
||||
/irc-proto/Cargo.lock
|
||||
*.json
|
||||
*.toml
|
||||
*.yaml
|
||||
!Cargo.toml
|
||||
|
|
16
.travis.yml
16
.travis.yml
|
@ -2,17 +2,11 @@ language: rust
|
|||
rust: stable
|
||||
sudo: false
|
||||
script:
|
||||
- chmod +x mktestconfig.sh
|
||||
- ./mktestconfig.sh
|
||||
- pushd irc-proto
|
||||
- cargo test --verbose
|
||||
- popd
|
||||
- cargo build --verbose
|
||||
- rustdoc --test README.md --extern irc=target/debug/libirc.rlib -L target/debug/deps
|
||||
- cargo test --verbose --features "toml yaml"
|
||||
- cargo test --doc
|
||||
- cargo test --verbose --no-default-features
|
||||
- cargo run --bin build-bot
|
||||
- cargo test --all --features "toml yaml json"
|
||||
- cargo build
|
||||
# No idea how to fix this, since we don't depend on futures-preview directly.
|
||||
# - rustdoc --test README.md --extern irc=target/debug/libirc.rlib -L target/debug/deps --edition 2018
|
||||
- cargo run --example build-bot
|
||||
notifications:
|
||||
email: false
|
||||
irc:
|
||||
|
|
20
Cargo.toml
20
Cargo.toml
|
@ -9,6 +9,7 @@ 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" }
|
||||
|
@ -31,22 +32,25 @@ bytes = "0.4"
|
|||
chrono = "0.4"
|
||||
encoding = "0.2"
|
||||
failure = "0.1"
|
||||
futures = "0.1"
|
||||
irc-proto = { version = "*", path = "irc-proto" }
|
||||
log = "0.4"
|
||||
native-tls = "0.2"
|
||||
serde = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_derive = "1.0"
|
||||
tokio = { version = "0.2.4", features = ["time", "net", "stream", "macros", "stream"] }
|
||||
tokio-util = { version = "0.2.0", features = ["codec"] }
|
||||
tokio-tls = "0.3.0"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde_yaml = { version = "0.7", optional = true }
|
||||
tokio = "0.1"
|
||||
tokio-codec = "0.1"
|
||||
tokio-io = "0.1"
|
||||
tokio-mockstream = "1.1"
|
||||
tokio-timer = "0.1"
|
||||
tokio-tls = "0.2"
|
||||
toml = { version = "0.4", optional = true }
|
||||
pin-utils = "0.1.0-alpha.4"
|
||||
parking_lot = "0.9.0"
|
||||
futures-channel = "0.3.1"
|
||||
futures-util = "0.3.1"
|
||||
|
||||
[dev-dependencies]
|
||||
futures = "0.3.1"
|
||||
anyhow = "1.0.13"
|
||||
args = "2.0"
|
||||
getopts = "0.2"
|
||||
env_logger = "0.6.2"
|
63
README.md
63
README.md
|
@ -48,21 +48,16 @@ documentation, and below.
|
|||
|
||||
[irc-prelude]: https://docs.rs/irc/*/irc/client/prelude/index.html
|
||||
|
||||
## A Tale of Two APIs
|
||||
## Using Futures
|
||||
|
||||
### Reactors (The "New" API)
|
||||
|
||||
The release of v0.13 brought with it a new API called `IrcReactor` that enables easier multiserver
|
||||
support and more graceful error handling. The general model is that you use the reactor to create
|
||||
new `IrcClients`, register message handler functions, and finally block the thread to run the
|
||||
clients with their respective handlers. Here's an example:
|
||||
|
||||
```rust,no_run
|
||||
extern crate irc;
|
||||
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::*;
|
||||
|
||||
fn main() {
|
||||
#[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()),
|
||||
|
@ -71,50 +66,16 @@ fn main() {
|
|||
..Config::default()
|
||||
};
|
||||
|
||||
let mut reactor = IrcReactor::new().unwrap();
|
||||
let client = reactor.prepare_client_and_connect(config).unwrap();
|
||||
client.identify().unwrap();
|
||||
let mut client = Client::from_config(config).await?;
|
||||
client.identify()?;
|
||||
|
||||
reactor.register_client_with_handler(client, |client, message| {
|
||||
let mut stream = client.stream()?;
|
||||
|
||||
while let Some(message) = stream.next().await.transpose()? {
|
||||
print!("{}", message);
|
||||
// And here we can do whatever we want with the messages.
|
||||
Ok(())
|
||||
});
|
||||
|
||||
reactor.run().unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Direct Style (The "Old" API)
|
||||
|
||||
The old API for connecting to an IRC server is still supported through the `IrcClient` type. It's
|
||||
simpler for the most basic use case, but will panic upon encountering any sort of connection issues.
|
||||
In general, it's recommended that users switch to the new API if possible. Nevertheless, here is an
|
||||
example:
|
||||
|
||||
```rust,no_run
|
||||
extern crate irc;
|
||||
|
||||
use std::default::Default;
|
||||
use irc::client::prelude::*;
|
||||
|
||||
fn main() {
|
||||
// We can also load the Config at runtime via Config::load("path/to/config.toml")
|
||||
let cfg = Config {
|
||||
nickname: Some(format!("the-irc-crate")),
|
||||
server: Some(format!("irc.example.com")),
|
||||
channels: Some(vec![format!("#test")]),
|
||||
.. Default::default()
|
||||
};
|
||||
|
||||
let client = IrcClient::from_config(cfg).unwrap();
|
||||
client.identify().unwrap();
|
||||
|
||||
client.for_each_incoming(|message| {
|
||||
print!("{}", message);
|
||||
// And here we can do whatever we want with the messages.
|
||||
}).unwrap()
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
extern crate irc;
|
||||
|
||||
use futures::prelude::*;
|
||||
use irc::client::prelude::*;
|
||||
use std::default::Default;
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
#[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();
|
||||
|
@ -17,11 +19,13 @@ fn main() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let mut reactor = IrcReactor::new().unwrap();
|
||||
let client = reactor.prepare_client_and_connect(config).unwrap();
|
||||
client.identify().unwrap();
|
||||
let mut client = Client::from_config(config).await?;
|
||||
|
||||
reactor.register_client_with_handler(client, move |client, message| {
|
||||
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(
|
||||
|
@ -34,13 +38,12 @@ fn main() {
|
|||
commit_message
|
||||
),
|
||||
)?;
|
||||
|
||||
client.send_quit("QUIT")?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
reactor.run().unwrap();
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
extern crate args;
|
||||
extern crate getopts;
|
||||
extern crate irc;
|
||||
|
||||
use std::env;
|
||||
use std::process::exit;
|
||||
|
||||
|
@ -34,18 +30,22 @@ fn main() {
|
|||
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",
|
||||
args.option(
|
||||
"i",
|
||||
"input",
|
||||
"The path to the input config",
|
||||
"FILE",
|
||||
Occur::Req,
|
||||
None);
|
||||
args.option("o",
|
||||
None,
|
||||
);
|
||||
args.option(
|
||||
"o",
|
||||
"output",
|
||||
"The path to output the new config to",
|
||||
"FILE",
|
||||
Occur::Req,
|
||||
None);
|
||||
None,
|
||||
);
|
||||
|
||||
args.parse(input)?;
|
||||
|
||||
|
@ -55,8 +55,5 @@ fn parse(input: &[String]) -> Result<Option<(String, String)>, ArgsError> {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some((
|
||||
args.value_of("input")?,
|
||||
args.value_of("output")?,
|
||||
)))
|
||||
Ok(Some((args.value_of("input")?, args.value_of("output")?)))
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
extern crate irc;
|
||||
|
||||
use std::default::Default;
|
||||
use irc::error;
|
||||
use irc::client::prelude::*;
|
||||
use futures::prelude::*;
|
||||
use irc::{client::prelude::*, error};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> irc::error::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
fn main() {
|
||||
let cfg1 = Config {
|
||||
nickname: Some("pickles".to_owned()),
|
||||
server: Some("irc.mozilla.org".to_owned()),
|
||||
|
@ -20,32 +22,39 @@ fn main() {
|
|||
};
|
||||
|
||||
let configs = vec![cfg1, cfg2];
|
||||
|
||||
let mut reactor = IrcReactor::new().unwrap();
|
||||
let mut senders = Vec::new();
|
||||
let mut streams = 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 client = reactor.prepare_client_and_connect(config).unwrap();
|
||||
client.identify().unwrap();
|
||||
// Here, we tell the reactor to setup this client for future handling (in run) using the specified
|
||||
// handler function process_msg.
|
||||
reactor.register_client_with_handler(client, process_msg);
|
||||
let mut client = Client::from_config(config).await?;
|
||||
client.identify()?;
|
||||
|
||||
senders.push(client.sender());
|
||||
streams.push(client.stream()?);
|
||||
}
|
||||
|
||||
// Runtime errors like a dropped connection will manifest here in the result of run.
|
||||
reactor.run().unwrap();
|
||||
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(client: &IrcClient, message: Message) -> error::Result<()> {
|
||||
print!("{}", 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") {
|
||||
client.send_privmsg(target, "Hi!")?;
|
||||
sender.send_privmsg(target, "Hi!")?;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
extern crate irc;
|
||||
|
||||
use std::default::Default;
|
||||
use irc::client::prelude::*;
|
||||
|
||||
// This example is meant to be a direct analogue to simple.rs using the reactor API.
|
||||
fn main() {
|
||||
let config = Config {
|
||||
nickname: Some("pickles".to_owned()),
|
||||
alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]),
|
||||
server: Some("irc.mozilla.org".to_owned()),
|
||||
channels: Some(vec!["#rust-spam".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut reactor = IrcReactor::new().unwrap();
|
||||
let client = reactor.prepare_client_and_connect(config).unwrap();
|
||||
client.identify().unwrap();
|
||||
|
||||
reactor.register_client_with_handler(client, |client, message| {
|
||||
print!("{}", message);
|
||||
if let Command::PRIVMSG(ref target, ref msg) = message.command {
|
||||
if msg.contains("pickles") {
|
||||
client.send_privmsg(target, "Hi!")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
reactor.run().unwrap();
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
extern crate irc;
|
||||
|
||||
use std::default::Default;
|
||||
use irc::error;
|
||||
use irc::client::prelude::*;
|
||||
|
||||
fn main() {
|
||||
let cfg1 = Config {
|
||||
nickname: Some("pickles".to_owned()),
|
||||
server: Some("irc.mozilla.org".to_owned()),
|
||||
channels: Some(vec!["#rust-spam".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cfg2 = Config {
|
||||
nickname: Some("bananas".to_owned()),
|
||||
server: Some("irc.mozilla.org".to_owned()),
|
||||
channels: Some(vec!["#rust-spam".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let configs = vec![cfg1, cfg2];
|
||||
|
||||
let mut reactor = IrcReactor::new().unwrap();
|
||||
|
||||
loop {
|
||||
let res = configs.iter().fold(Ok(()), |acc, config| {
|
||||
acc.and(
|
||||
reactor.prepare_client_and_connect(config.clone()).and_then(|client| {
|
||||
client.identify().and(Ok(client))
|
||||
}).and_then(|client| {
|
||||
reactor.register_client_with_handler(client, process_msg);
|
||||
Ok(())
|
||||
})
|
||||
)
|
||||
}).and_then(|()| reactor.run());
|
||||
|
||||
match res {
|
||||
// The connections ended normally (for example, they sent a QUIT message to the server).
|
||||
Ok(_) => break,
|
||||
// Something went wrong! We'll print the error, and restart the connections.
|
||||
Err(e) => eprintln!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> {
|
||||
print!("{}", message);
|
||||
if let Command::PRIVMSG(ref target, ref msg) = message.command {
|
||||
if msg.contains("pickles") {
|
||||
client.send_privmsg(target, "Hi!")?;
|
||||
} else if msg.contains("quit") {
|
||||
client.send_quit("bye")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
extern crate irc;
|
||||
|
||||
use std::default::Default;
|
||||
use futures::prelude::*;
|
||||
use irc::client::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() -> irc::error::Result<()> {
|
||||
let config = Config {
|
||||
nickname: Some("repeater".to_owned()),
|
||||
alt_nicks: Some(vec!["blaster".to_owned(), "smg".to_owned()]),
|
||||
|
@ -15,11 +14,14 @@ fn main() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let client = IrcClient::from_config(config).unwrap();
|
||||
client.identify().unwrap();
|
||||
let mut client = Client::from_config(config).await?;
|
||||
client.identify()?;
|
||||
|
||||
let mut stream = client.stream()?;
|
||||
|
||||
loop {
|
||||
let message = stream.select_next_some().await?;
|
||||
|
||||
client.for_each_incoming(|message| {
|
||||
print!("{}", message);
|
||||
if let Command::PRIVMSG(ref target, ref msg) = message.command {
|
||||
if msg.starts_with(&*client.current_nickname()) {
|
||||
let tokens: Vec<_> = msg.split(' ').collect();
|
||||
|
@ -29,12 +31,12 @@ fn main() {
|
|||
for _ in 0..count {
|
||||
client.send_privmsg(
|
||||
message.response_target().unwrap_or(target),
|
||||
&msg[n..]
|
||||
).unwrap();
|
||||
&msg[n..],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).unwrap()
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
extern crate irc;
|
||||
|
||||
use std::default::Default;
|
||||
use futures::prelude::*;
|
||||
use irc::client::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() -> irc::error::Result<()> {
|
||||
let config = Config {
|
||||
nickname: Some("pickles".to_owned()),
|
||||
alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]),
|
||||
|
@ -12,15 +11,18 @@ fn main() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let client = IrcClient::from_config(config).unwrap();
|
||||
client.identify().unwrap();
|
||||
let mut client = Client::from_config(config).await?;
|
||||
client.identify()?;
|
||||
|
||||
let mut stream = client.stream()?;
|
||||
|
||||
loop {
|
||||
let message = stream.select_next_some().await?;
|
||||
|
||||
client.for_each_incoming(|message| {
|
||||
print!("{}", message);
|
||||
if let Command::PRIVMSG(ref target, ref msg) = message.command {
|
||||
if msg.contains("pickles") {
|
||||
client.send_privmsg(target, "Hi!").unwrap();
|
||||
}
|
||||
}
|
||||
}).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
extern crate irc;
|
||||
|
||||
use std::default::Default;
|
||||
use futures::prelude::*;
|
||||
use irc::client::prelude::*;
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() -> irc::error::Result<()> {
|
||||
let config = Config {
|
||||
nickname: Some("pickles".to_owned()),
|
||||
server: Some("irc.mozilla.org".to_owned()),
|
||||
|
@ -12,15 +11,17 @@ fn main() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let client = IrcClient::from_config(config).unwrap();
|
||||
client.identify().unwrap();
|
||||
let mut client = Client::from_config(config).await?;
|
||||
let mut stream = client.stream()?;
|
||||
let sender = client.sender();
|
||||
|
||||
loop {
|
||||
let message = stream.select_next_some().await?;
|
||||
|
||||
client.for_each_incoming(|message| {
|
||||
print!("{}", message);
|
||||
if let Command::PRIVMSG(ref target, ref msg) = message.command {
|
||||
if msg.contains("pickles") {
|
||||
client.send_privmsg(target, "Hi!").unwrap();
|
||||
sender.send_privmsg(target, "Hi!")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).unwrap();
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
extern crate irc;
|
||||
extern crate tokio_timer;
|
||||
|
||||
use std::default::Default;
|
||||
use std::time::Duration;
|
||||
use futures::prelude::*;
|
||||
use irc::client::prelude::*;
|
||||
use irc::error::IrcError;
|
||||
use std::time::Duration;
|
||||
|
||||
// NOTE: this example is a conversion of `tweeter.rs` to an asynchronous style with `IrcReactor`.
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() -> irc::error::Result<()> {
|
||||
let config = Config {
|
||||
nickname: Some("mastodon".to_owned()),
|
||||
server: Some("irc.mozilla.org".to_owned()),
|
||||
|
@ -15,37 +12,13 @@ fn main() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
// We need to create a reactor first and foremost
|
||||
let mut reactor = IrcReactor::new().unwrap();
|
||||
// and then create a client via its API.
|
||||
let client = reactor.prepare_client_and_connect(config).unwrap();
|
||||
// Then, we identify
|
||||
client.identify().unwrap();
|
||||
// and clone just as before.
|
||||
let send_client = client.clone();
|
||||
let client = Client::from_config(config).await?;
|
||||
let sender = client.sender();
|
||||
|
||||
// Rather than spawn a thread that reads the messages separately, we register a handler with the
|
||||
// reactor. just as in the original version, we don't do any real handling and instead just
|
||||
// print the messages that are received.
|
||||
reactor.register_client_with_handler(client, |_, message| {
|
||||
print!("{}", message);
|
||||
Ok(())
|
||||
});
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(1)).fuse();
|
||||
|
||||
// We construct an interval using a wheel timer from tokio_timer. This interval will fire every
|
||||
// ten seconds (and is roughly accurate to the second).
|
||||
let send_interval = tokio_timer::wheel()
|
||||
.tick_duration(Duration::from_secs(1))
|
||||
.num_slots(256)
|
||||
.build()
|
||||
.interval(Duration::from_secs(10));
|
||||
|
||||
// And then spawn a new future that performs the given action each time it fires.
|
||||
reactor.register_future(send_interval.map_err(IrcError::Timer).for_each(move |()| {
|
||||
// Anything in here will happen every 10 seconds!
|
||||
send_client.send_privmsg("#rust-spam", "AWOOOOOOOOOO")
|
||||
}));
|
||||
|
||||
// Then, on the main thread, we finally run the reactor which blocks the program indefinitely.
|
||||
reactor.run().unwrap();
|
||||
loop {
|
||||
let _ = interval.select_next_some().await;
|
||||
sender.send_privmsg("#rust-spam", "AWOOOOOOOOOO")?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
extern crate irc;
|
||||
|
||||
use std::default::Default;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
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`.
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() -> irc::error::Result<()> {
|
||||
let config = Config {
|
||||
nickname: Some("pickles".to_owned()),
|
||||
server: Some("irc.mozilla.org".to_owned()),
|
||||
channels: Some(vec!["#rust-spam".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
let client = IrcClient::from_config(config).unwrap();
|
||||
client.identify().unwrap();
|
||||
let client2 = client.clone();
|
||||
// Let's set up a loop that just prints the messages.
|
||||
thread::spawn(move || {
|
||||
client2.stream().map(|m| print!("{}", m)).wait().count();
|
||||
});
|
||||
|
||||
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 {
|
||||
client.send_privmsg("#rust-spam", "TWEET TWEET").unwrap();
|
||||
thread::sleep(Duration::new(10, 0));
|
||||
futures::select! {
|
||||
m = stream.select_next_some() => {
|
||||
println!("{}", m?);
|
||||
}
|
||||
_ = interval.select_next_some() => {
|
||||
client.send_privmsg("#rust-spam", "TWEET TWEET")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,11 @@ repository = "https://github.com/aatxe/irc"
|
|||
travis-ci = { repository = "aatxe/irc" }
|
||||
|
||||
[features]
|
||||
default = ["tokio"]
|
||||
tokio = ["tokio-codec", "tokio-io", "bytes"]
|
||||
default = ["tokio", "tokio-util", "bytes"]
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "0.4", optional = true }
|
||||
bytes = { version = "0.5", optional = true }
|
||||
encoding = "0.2"
|
||||
failure = "0.1"
|
||||
tokio-codec = { version = "0.1", optional = true }
|
||||
tokio-io = { version = "0.1", optional = true }
|
||||
tokio-util = { version = "0.2.0", optional = true }
|
||||
tokio = { version = "0.2.0", optional = true }
|
||||
|
|
|
@ -8,10 +8,10 @@ pub trait ChannelExt {
|
|||
|
||||
impl<'a> ChannelExt for &'a str {
|
||||
fn is_channel_name(&self) -> bool {
|
||||
self.starts_with('#') ||
|
||||
self.starts_with('&') ||
|
||||
self.starts_with('+') ||
|
||||
self.starts_with('!')
|
||||
self.starts_with('#')
|
||||
|| self.starts_with('&')
|
||||
|| self.starts_with('+')
|
||||
|| self.starts_with('!')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,13 +15,11 @@ struct Parser {
|
|||
|
||||
/// 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] = &[
|
||||
|
@ -66,9 +64,7 @@ impl Parser {
|
|||
self.state = ColorCode;
|
||||
false
|
||||
}
|
||||
Text => {
|
||||
!FORMAT_CHARACTERS.contains(&cur)
|
||||
}
|
||||
Text => !FORMAT_CHARACTERS.contains(&cur),
|
||||
ColorCode if cur.is_digit(10) => {
|
||||
self.state = Foreground1(cur);
|
||||
false
|
||||
|
@ -122,8 +118,8 @@ impl FormattedStringExt<'static> for String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::borrow::Cow;
|
||||
use colors::FormattedStringExt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
macro_rules! test_formatted_string_ext {
|
||||
{ $( $name:ident ( $($line:tt)* ), )* } => {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
//! Enumeration of all available client commands.
|
||||
use std::str::FromStr;
|
||||
|
||||
use error::MessageParseError;
|
||||
use chan::ChannelExt;
|
||||
use error::MessageParseError;
|
||||
use mode::{ChannelMode, Mode, UserMode};
|
||||
use response::Response;
|
||||
|
||||
|
@ -170,7 +170,12 @@ pub enum Command {
|
|||
|
||||
// IRCv3 support
|
||||
/// CAP [*] COMMAND [*] :[param]
|
||||
CAP(Option<String>, CapSubCommand, Option<String>, Option<String>),
|
||||
CAP(
|
||||
Option<String>,
|
||||
CapSubCommand,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
),
|
||||
|
||||
// IRCv3.1 extensions
|
||||
/// AUTHENTICATE data
|
||||
|
@ -184,7 +189,12 @@ pub enum Command {
|
|||
|
||||
// IRCv3.2 extensions
|
||||
/// METADATA target COMMAND [params] :[param]
|
||||
METADATA(String, Option<MetadataSubCommand>, Option<Vec<String>>, Option<String>),
|
||||
METADATA(
|
||||
String,
|
||||
Option<MetadataSubCommand>,
|
||||
Option<Vec<String>>,
|
||||
Option<String>,
|
||||
),
|
||||
/// MONITOR command [nicklist]
|
||||
MONITOR(String, Option<String>),
|
||||
/// BATCH (+/-)reference-tag [type [params]]
|
||||
|
@ -206,7 +216,6 @@ fn stringify(cmd: &str, args: &[&str], suffix: Option<&str>) -> String {
|
|||
Some(suffix) => format!("{}{}{} :{}", cmd, sp, args, suffix),
|
||||
None => format!("{}{}{}", cmd, sp, args),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Command> for String {
|
||||
|
@ -216,13 +225,15 @@ impl<'a> From<&'a Command> for String {
|
|||
Command::NICK(ref n) => stringify("NICK", &[], Some(n)),
|
||||
Command::USER(ref u, ref m, ref r) => stringify("USER", &[u, m, "*"], Some(r)),
|
||||
Command::OPER(ref u, ref p) => stringify("OPER", &[u], Some(p)),
|
||||
Command::UserMODE(ref u, ref m) => {
|
||||
format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| {
|
||||
Command::UserMODE(ref u, ref m) => format!(
|
||||
"MODE {}{}",
|
||||
u,
|
||||
m.iter().fold(String::new(), |mut acc, mode| {
|
||||
acc.push_str(" ");
|
||||
acc.push_str(&mode.to_string());
|
||||
acc
|
||||
}))
|
||||
}
|
||||
})
|
||||
),
|
||||
Command::SERVICE(ref n, ref r, ref d, ref t, ref re, ref i) => {
|
||||
stringify("SERVICE", &[n, r, d, t, re], Some(i))
|
||||
}
|
||||
|
@ -235,13 +246,15 @@ impl<'a> From<&'a Command> for String {
|
|||
Command::JOIN(ref c, None, None) => stringify("JOIN", &[c], None),
|
||||
Command::PART(ref c, Some(ref m)) => stringify("PART", &[c], Some(m)),
|
||||
Command::PART(ref c, None) => stringify("PART", &[c], None),
|
||||
Command::ChannelMODE(ref u, ref m) => {
|
||||
format!("MODE {}{}", u, m.iter().fold(String::new(), |mut acc, mode| {
|
||||
Command::ChannelMODE(ref u, ref m) => format!(
|
||||
"MODE {}{}",
|
||||
u,
|
||||
m.iter().fold(String::new(), |mut acc, mode| {
|
||||
acc.push_str(" ");
|
||||
acc.push_str(&mode.to_string());
|
||||
acc
|
||||
}))
|
||||
}
|
||||
})
|
||||
),
|
||||
Command::TOPIC(ref c, Some(ref t)) => stringify("TOPIC", &[c], Some(t)),
|
||||
Command::TOPIC(ref c, None) => stringify("TOPIC", &[c], None),
|
||||
Command::NAMES(Some(ref c), Some(ref t)) => stringify("NAMES", &[c], Some(t)),
|
||||
|
@ -311,13 +324,11 @@ impl<'a> From<&'a Command> for String {
|
|||
Command::USERS(Some(ref t)) => stringify("USERS", &[], Some(t)),
|
||||
Command::USERS(None) => stringify("USERS", &[], None),
|
||||
Command::WALLOPS(ref t) => stringify("WALLOPS", &[], Some(t)),
|
||||
Command::USERHOST(ref u) => {
|
||||
stringify(
|
||||
Command::USERHOST(ref u) => stringify(
|
||||
"USERHOST",
|
||||
&u.iter().map(|s| &s[..]).collect::<Vec<_>>(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
),
|
||||
Command::ISON(ref u) => {
|
||||
stringify("ISON", &u.iter().map(|s| &s[..]).collect::<Vec<_>>(), None)
|
||||
}
|
||||
|
@ -369,8 +380,7 @@ impl<'a> From<&'a Command> for String {
|
|||
stringify("METADATA", &[&t[..], c.to_str()], None)
|
||||
}
|
||||
|
||||
Command::METADATA(ref t, Some(ref c), Some(ref a), Some(ref p)) => {
|
||||
stringify(
|
||||
Command::METADATA(ref t, Some(ref c), Some(ref a), Some(ref p)) => stringify(
|
||||
"METADATA",
|
||||
&vec![t, &c.to_str().to_owned()]
|
||||
.iter()
|
||||
|
@ -378,10 +388,8 @@ impl<'a> From<&'a Command> for String {
|
|||
.chain(a.iter().map(|s| &s[..]))
|
||||
.collect::<Vec<_>>(),
|
||||
Some(p),
|
||||
)
|
||||
}
|
||||
Command::METADATA(ref t, Some(ref c), Some(ref a), None) => {
|
||||
stringify(
|
||||
),
|
||||
Command::METADATA(ref t, Some(ref c), Some(ref a), None) => stringify(
|
||||
"METADATA",
|
||||
&vec![t, &c.to_str().to_owned()]
|
||||
.iter()
|
||||
|
@ -389,14 +397,12 @@ impl<'a> From<&'a Command> for String {
|
|||
.chain(a.iter().map(|s| &s[..]))
|
||||
.collect::<Vec<_>>(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
),
|
||||
Command::METADATA(ref t, None, None, Some(ref p)) => {
|
||||
stringify("METADATA", &[t], Some(p))
|
||||
}
|
||||
Command::METADATA(ref t, None, None, None) => stringify("METADATA", &[t], None),
|
||||
Command::METADATA(ref t, None, Some(ref a), Some(ref p)) => {
|
||||
stringify(
|
||||
Command::METADATA(ref t, None, Some(ref a), Some(ref p)) => stringify(
|
||||
"METADATA",
|
||||
&vec![t]
|
||||
.iter()
|
||||
|
@ -404,10 +410,8 @@ impl<'a> From<&'a Command> for String {
|
|||
.chain(a.iter().map(|s| &s[..]))
|
||||
.collect::<Vec<_>>(),
|
||||
Some(p),
|
||||
)
|
||||
}
|
||||
Command::METADATA(ref t, None, Some(ref a), None) => {
|
||||
stringify(
|
||||
),
|
||||
Command::METADATA(ref t, None, Some(ref a), None) => stringify(
|
||||
"METADATA",
|
||||
&vec![t]
|
||||
.iter()
|
||||
|
@ -415,12 +419,10 @@ impl<'a> From<&'a Command> for String {
|
|||
.chain(a.iter().map(|s| &s[..]))
|
||||
.collect::<Vec<_>>(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
),
|
||||
Command::MONITOR(ref c, Some(ref t)) => stringify("MONITOR", &[c, t], None),
|
||||
Command::MONITOR(ref c, None) => stringify("MONITOR", &[c], None),
|
||||
Command::BATCH(ref t, Some(ref c), Some(ref a)) => {
|
||||
stringify(
|
||||
Command::BATCH(ref t, Some(ref c), Some(ref a)) => stringify(
|
||||
"BATCH",
|
||||
&vec![t, &c.to_str().to_owned()]
|
||||
.iter()
|
||||
|
@ -428,11 +430,9 @@ impl<'a> From<&'a Command> for String {
|
|||
.chain(a.iter().map(|s| &s[..]))
|
||||
.collect::<Vec<_>>(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
),
|
||||
Command::BATCH(ref t, Some(ref c), None) => stringify("BATCH", &[t, c.to_str()], None),
|
||||
Command::BATCH(ref t, None, Some(ref a)) => {
|
||||
stringify(
|
||||
Command::BATCH(ref t, None, Some(ref a)) => stringify(
|
||||
"BATCH",
|
||||
&vec![t]
|
||||
.iter()
|
||||
|
@ -440,21 +440,20 @@ impl<'a> From<&'a Command> for String {
|
|||
.chain(a.iter().map(|s| &s[..]))
|
||||
.collect::<Vec<_>>(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
),
|
||||
Command::BATCH(ref t, None, None) => stringify("BATCH", &[t], None),
|
||||
Command::CHGHOST(ref u, ref h) => stringify("CHGHOST", &[u, h], None),
|
||||
|
||||
Command::Response(ref resp, ref a, Some(ref s)) => {
|
||||
stringify(&format!("{:03}", *resp as u16),
|
||||
Command::Response(ref resp, ref a, Some(ref s)) => stringify(
|
||||
&format!("{:03}", *resp as u16),
|
||||
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
|
||||
Some(s))
|
||||
}
|
||||
Command::Response(ref resp, ref a, None) => {
|
||||
stringify(&format!("{:03}", *resp as u16),
|
||||
Some(s),
|
||||
),
|
||||
Command::Response(ref resp, ref a, None) => stringify(
|
||||
&format!("{:03}", *resp as u16),
|
||||
&a.iter().map(|s| &s[..]).collect::<Vec<_>>(),
|
||||
None)
|
||||
}
|
||||
None,
|
||||
),
|
||||
Command::Raw(ref c, ref a, Some(ref s)) => {
|
||||
stringify(c, &a.iter().map(|s| &s[..]).collect::<Vec<_>>(), Some(s))
|
||||
}
|
||||
|
@ -467,7 +466,11 @@ impl<'a> From<&'a Command> for String {
|
|||
|
||||
impl Command {
|
||||
/// Constructs a new Command.
|
||||
pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> Result<Command, MessageParseError> {
|
||||
pub fn new(
|
||||
cmd: &str,
|
||||
args: Vec<&str>,
|
||||
suffix: Option<&str>,
|
||||
) -> Result<Command, MessageParseError> {
|
||||
Ok(if cmd.eq_ignore_ascii_case("PASS") {
|
||||
match suffix {
|
||||
Some(suffix) => {
|
||||
|
@ -539,13 +542,15 @@ impl Command {
|
|||
} else if cmd.eq_ignore_ascii_case("MODE") {
|
||||
match suffix {
|
||||
Some(suffix) => raw(cmd, args, Some(suffix)),
|
||||
None => if args[0].is_channel_name() {
|
||||
None => {
|
||||
if args[0].is_channel_name() {
|
||||
let arg = args[1..].join(" ");
|
||||
Command::ChannelMODE(args[0].to_owned(), Mode::as_channel_modes(&arg)?)
|
||||
} else {
|
||||
let arg = args[1..].join(" ");
|
||||
Command::UserMODE(args[0].to_owned(), Mode::as_user_modes(&arg)?)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if cmd.eq_ignore_ascii_case("SERVICE") {
|
||||
match suffix {
|
||||
|
@ -1432,26 +1437,22 @@ impl Command {
|
|||
} else if args.len() == 2 {
|
||||
if let Ok(cmd) = args[0].parse() {
|
||||
match suffix {
|
||||
Some(suffix) => {
|
||||
Command::CAP(
|
||||
Some(suffix) => Command::CAP(
|
||||
None,
|
||||
cmd,
|
||||
Some(args[1].to_owned()),
|
||||
Some(suffix.to_owned()),
|
||||
)
|
||||
}
|
||||
),
|
||||
None => Command::CAP(None, cmd, Some(args[1].to_owned()), None),
|
||||
}
|
||||
} else if let Ok(cmd) = args[1].parse() {
|
||||
match suffix {
|
||||
Some(suffix) => {
|
||||
Command::CAP(
|
||||
Some(suffix) => Command::CAP(
|
||||
Some(args[0].to_owned()),
|
||||
cmd,
|
||||
None,
|
||||
Some(suffix.to_owned()),
|
||||
)
|
||||
}
|
||||
),
|
||||
None => Command::CAP(Some(args[0].to_owned()), cmd, None, None),
|
||||
}
|
||||
} else {
|
||||
|
@ -1460,22 +1461,18 @@ impl Command {
|
|||
} else if args.len() == 3 {
|
||||
if let Ok(cmd) = args[1].parse() {
|
||||
match suffix {
|
||||
Some(suffix) => {
|
||||
Command::CAP(
|
||||
Some(suffix) => Command::CAP(
|
||||
Some(args[0].to_owned()),
|
||||
cmd,
|
||||
Some(args[2].to_owned()),
|
||||
Some(suffix.to_owned()),
|
||||
)
|
||||
}
|
||||
None => {
|
||||
Command::CAP(
|
||||
),
|
||||
None => Command::CAP(
|
||||
Some(args[0].to_owned()),
|
||||
cmd,
|
||||
Some(args[2].to_owned()),
|
||||
None,
|
||||
)
|
||||
}
|
||||
),
|
||||
}
|
||||
} else {
|
||||
raw(cmd, args, suffix)
|
||||
|
@ -1521,23 +1518,19 @@ impl Command {
|
|||
if args.len() == 2 {
|
||||
match suffix {
|
||||
Some(_) => raw(cmd, args, suffix),
|
||||
None => {
|
||||
match args[1].parse() {
|
||||
None => match args[1].parse() {
|
||||
Ok(c) => Command::METADATA(args[0].to_owned(), Some(c), None, None),
|
||||
Err(_) => raw(cmd, args, suffix),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
} else if args.len() > 2 {
|
||||
match args[1].parse() {
|
||||
Ok(c) => {
|
||||
Command::METADATA(
|
||||
Ok(c) => Command::METADATA(
|
||||
args[0].to_owned(),
|
||||
Some(c),
|
||||
Some(args.into_iter().skip(1).map(|s| s.to_owned()).collect()),
|
||||
suffix.map(|s| s.to_owned()),
|
||||
)
|
||||
}
|
||||
),
|
||||
Err(_) => {
|
||||
if args.len() == 3 && suffix.is_some() {
|
||||
Command::METADATA(
|
||||
|
@ -1787,15 +1780,19 @@ impl FromStr for BatchSubCommand {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::Message;
|
||||
use super::Response;
|
||||
use super::Command;
|
||||
use super::Response;
|
||||
use crate::Message;
|
||||
|
||||
#[test]
|
||||
fn format_response() {
|
||||
assert!(String::from(&Command::Response(Response::RPL_WELCOME,
|
||||
assert!(
|
||||
String::from(&Command::Response(
|
||||
Response::RPL_WELCOME,
|
||||
vec!["foo".into()],
|
||||
None)) == "001 foo");
|
||||
None
|
||||
)) == "001 foo"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1809,6 +1806,9 @@ mod test {
|
|||
#[test]
|
||||
fn parse_user_message() {
|
||||
let cmd = "USER a 0 * b".parse::<Message>().unwrap().command;
|
||||
assert_eq!(Command::USER("a".to_string(), "0".to_string(), "b".to_string()), cmd);
|
||||
assert_eq!(
|
||||
Command::USER("a".to_string(), "0".to_string(), "b".to_string()),
|
||||
cmd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ pub enum MessageParseError {
|
|||
cmd: &'static str,
|
||||
/// The invalid subcommand.
|
||||
sub: String,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/// Errors that occur while parsing mode strings.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Implementation of IRC codec for Tokio.
|
||||
use bytes::BytesMut;
|
||||
use tokio_codec::{Decoder, Encoder};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use error;
|
||||
use line::LineCodec;
|
||||
|
@ -31,7 +31,6 @@ impl IrcCodec {
|
|||
}
|
||||
data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Decoder for IrcCodec {
|
||||
|
@ -39,9 +38,9 @@ impl Decoder for IrcCodec {
|
|||
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))
|
||||
})
|
||||
self.inner
|
||||
.decode(src)
|
||||
.and_then(|res| res.map_or(Ok(None), |msg| msg.parse::<Message>().map(Some)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +48,6 @@ impl Encoder for IrcCodec {
|
|||
type Item = Message;
|
||||
type Error = error::ProtocolError;
|
||||
|
||||
|
||||
fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> {
|
||||
self.inner.encode(IrcCodec::sanitize(msg.to_string()), dst)
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@ extern crate encoding;
|
|||
#[macro_use]
|
||||
extern crate failure;
|
||||
#[cfg(feature = "tokio")]
|
||||
extern crate tokio_codec;
|
||||
#[cfg(feature = "tokio")]
|
||||
extern crate tokio_io;
|
||||
extern crate tokio;
|
||||
#[cfg(feature = "tokio-util")]
|
||||
extern crate tokio_util;
|
||||
|
||||
pub mod caps;
|
||||
pub mod chan;
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
use std::io;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use encoding::{DecoderTrap, EncoderTrap, EncodingRef};
|
||||
use encoding::label::encoding_from_whatwg_label;
|
||||
use tokio_io::codec::{Decoder, Encoder};
|
||||
use encoding::{DecoderTrap, EncoderTrap, EncodingRef};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use error;
|
||||
|
||||
|
@ -19,11 +19,17 @@ 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(
|
||||
.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())
|
||||
)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,12 +48,11 @@ impl Decoder for LineCodec {
|
|||
// 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(
|
||||
Err(data) => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
&format!("Failed to decode {} as {}.", data, self.encoding.name())[..],
|
||||
).into(),
|
||||
),
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
} else {
|
||||
// Set the search start index to the current length since we know that none of the
|
||||
|
@ -64,13 +69,15 @@ impl Encoder for LineCodec {
|
|||
|
||||
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
|
||||
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()
|
||||
)
|
||||
.into()
|
||||
});
|
||||
|
||||
// Write the encoded message to the output buffer.
|
||||
|
|
|
@ -9,7 +9,6 @@ use error;
|
|||
use error::{MessageParseError, ProtocolError};
|
||||
use 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
|
||||
|
@ -85,7 +84,7 @@ impl Message {
|
|||
// <servername> ::= <host>
|
||||
self.prefix.as_ref().and_then(|p| match p {
|
||||
Prefix::Nickname(name, _, _) => Some(&name[..]),
|
||||
_ => None
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -112,7 +111,7 @@ impl Message {
|
|||
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()
|
||||
_ => self.source_nickname(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,7 +171,7 @@ impl FromStr for Message {
|
|||
return Err(ProtocolError::InvalidMessage {
|
||||
string: s.to_owned(),
|
||||
cause: MessageParseError::EmptyMessage,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let mut state = s;
|
||||
|
@ -210,10 +209,13 @@ impl FromStr for Message {
|
|||
"\n"
|
||||
} else {
|
||||
""
|
||||
}.len();
|
||||
}
|
||||
.len();
|
||||
|
||||
let suffix = if state.contains(" :") {
|
||||
let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - line_ending_len]);
|
||||
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 {
|
||||
|
@ -227,16 +229,18 @@ impl FromStr for Message {
|
|||
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 {
|
||||
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 args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect();
|
||||
|
@ -272,7 +276,7 @@ pub struct Tag(pub String, pub Option<String>);
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Message, Tag};
|
||||
use command::Command::{PRIVMSG, QUIT, Raw};
|
||||
use command::Command::{Raw, PRIVMSG, QUIT};
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
|
@ -428,7 +432,8 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn from_and_to_string() {
|
||||
let message = "@aaa=bbb;ccc;example.com/ddd=eee :test!test@test PRIVMSG test :Testing with \
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
//! A module defining an API for IRC user and channel modes.
|
||||
use std::fmt;
|
||||
|
||||
use command::Command;
|
||||
use error::MessageParseError;
|
||||
use error::MessageParseError::InvalidModeString;
|
||||
use error::ModeParseError::*;
|
||||
use command::Command;
|
||||
|
||||
/// A marker trait for different kinds of Modes.
|
||||
pub trait ModeType: fmt::Display + fmt::Debug + Clone + PartialEq {
|
||||
|
@ -71,7 +71,10 @@ impl fmt::Display for UserMode {
|
|||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::UserMode::*;
|
||||
|
||||
write!(f, "{}", match *self {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
Away => 'a',
|
||||
Invisible => 'i',
|
||||
Wallops => 'w',
|
||||
|
@ -81,7 +84,8 @@ impl fmt::Display for UserMode {
|
|||
ServerNotices => 's',
|
||||
MaskedHost => 'x',
|
||||
Unknown(c) => c,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,8 +139,8 @@ impl ModeType for ChannelMode {
|
|||
use self::ChannelMode::*;
|
||||
|
||||
match *self {
|
||||
Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop |
|
||||
Voice => true,
|
||||
Ban | Exception | Limit | InviteException | Key | Founder | Admin | Oper | Halfop
|
||||
| Voice => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +176,10 @@ impl fmt::Display for ChannelMode {
|
|||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::ChannelMode::*;
|
||||
|
||||
write!(f, "{}", match *self {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
Ban => 'b',
|
||||
Exception => 'e',
|
||||
Limit => 'l',
|
||||
|
@ -190,7 +197,8 @@ impl fmt::Display for ChannelMode {
|
|||
Halfop => 'h',
|
||||
Voice => 'v',
|
||||
Unknown(c) => c,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,14 +265,18 @@ impl Mode<UserMode> {
|
|||
let init = match chars.next() {
|
||||
Some('+') => Plus,
|
||||
Some('-') => Minus,
|
||||
Some(c) => return Err(InvalidModeString {
|
||||
Some(c) => {
|
||||
return Err(InvalidModeString {
|
||||
string: s.to_owned(),
|
||||
cause: InvalidModeModifier { modifier: c },
|
||||
}),
|
||||
None => return Err(InvalidModeString {
|
||||
})
|
||||
}
|
||||
None => {
|
||||
return Err(InvalidModeString {
|
||||
string: s.to_owned(),
|
||||
cause: MissingModeModifier,
|
||||
}),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
for c in chars {
|
||||
|
@ -303,14 +315,18 @@ impl Mode<ChannelMode> {
|
|||
let init = match chars.next() {
|
||||
Some('+') => Plus,
|
||||
Some('-') => Minus,
|
||||
Some(c) => return Err(InvalidModeString {
|
||||
Some(c) => {
|
||||
return Err(InvalidModeString {
|
||||
string: s.to_owned(),
|
||||
cause: InvalidModeModifier { modifier: c },
|
||||
}),
|
||||
None => return Err(InvalidModeString {
|
||||
})
|
||||
}
|
||||
None => {
|
||||
return Err(InvalidModeString {
|
||||
string: s.to_owned(),
|
||||
cause: MissingModeModifier,
|
||||
}),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
for c in chars {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! A module providing an enum for a message prefix.
|
||||
use std::str::FromStr;
|
||||
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)]
|
||||
|
@ -50,12 +50,12 @@ impl Prefix {
|
|||
'!' 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
|
||||
|
@ -63,7 +63,8 @@ impl Prefix {
|
|||
Active::Name => &mut name,
|
||||
Active::User => &mut user,
|
||||
Active::Host => &mut host,
|
||||
}.push(c)
|
||||
}
|
||||
.push(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +110,7 @@ impl<'a> From<&'a str> for Prefix {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Prefix::{self, ServerName, Nickname};
|
||||
use super::Prefix::{self, Nickname, ServerName};
|
||||
|
||||
// Checks that str -> parsed -> Display doesn't lose data
|
||||
fn test_parse(s: &str) -> Prefix {
|
||||
|
@ -139,10 +140,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn parse_host() {
|
||||
assert_eq!(
|
||||
test_parse("host.tld"),
|
||||
ServerName("host.tld".into())
|
||||
)
|
||||
assert_eq!(test_parse("host.tld"), ServerName("host.tld".into()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.test.net\",\"port\": 6667,\"use_ssl\": false,\"encoding\": \"UTF-8\",\"channels\": [\"#test\", \"#test2\"],\"umodes\": \"+BR\",\"options\": {}}" > client_config.json
|
||||
cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.toml
|
||||
cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.yaml
|
|
@ -1,31 +1,35 @@
|
|||
//! A module providing IRC connections for use by `IrcServer`s.
|
||||
use std::fs::File;
|
||||
use std::fmt;
|
||||
use std::io::Read;
|
||||
|
||||
use encoding::EncoderTrap;
|
||||
use encoding::label::encoding_from_whatwg_label;
|
||||
use futures::{Async, Poll, Future, Sink, StartSend, Stream};
|
||||
use native_tls::{Certificate, TlsConnector, Identity};
|
||||
use tokio_codec::Decoder;
|
||||
use tokio::net::{TcpStream};
|
||||
use tokio::net::tcp::ConnectFuture;
|
||||
use tokio_mockstream::MockStream;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use futures_util::{sink::Sink, stream::Stream};
|
||||
use native_tls::{Certificate, Identity, TlsConnector};
|
||||
use std::{
|
||||
fmt,
|
||||
fs::File,
|
||||
io::Read,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_tls::{self, TlsStream};
|
||||
use tokio_util::codec::Decoder;
|
||||
|
||||
use error;
|
||||
use client::data::Config;
|
||||
use client::transport::{IrcTransport, LogView, Logged};
|
||||
use proto::{IrcCodec, Message};
|
||||
use crate::{
|
||||
client::{
|
||||
data::Config,
|
||||
transport::{LogView, Logged, Transport},
|
||||
},
|
||||
error,
|
||||
proto::{IrcCodec, Message},
|
||||
};
|
||||
|
||||
/// An IRC connection used internally by `IrcServer`.
|
||||
pub enum Connection {
|
||||
#[doc(hidden)]
|
||||
Unsecured(IrcTransport<TcpStream>),
|
||||
Unsecured(Transport<TcpStream>),
|
||||
#[doc(hidden)]
|
||||
Secured(IrcTransport<TlsStream<TcpStream>>),
|
||||
Secured(Transport<TlsStream<TcpStream>>),
|
||||
#[doc(hidden)]
|
||||
Mock(Logged<MockStream>),
|
||||
Mock(Logged<crate::client::mock::MockStream>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for Connection {
|
||||
|
@ -42,102 +46,49 @@ impl fmt::Debug for Connection {
|
|||
}
|
||||
}
|
||||
|
||||
/// A convenient type alias representing the `TlsStream` future.
|
||||
type TlsFuture = Box<Future<Error = error::IrcError, Item = TlsStream<TcpStream>> + Send>;
|
||||
|
||||
/// A future representing an eventual `Connection`.
|
||||
pub enum ConnectionFuture {
|
||||
#[doc(hidden)]
|
||||
Unsecured(Config, ConnectFuture),
|
||||
#[doc(hidden)]
|
||||
Secured(Config, TlsFuture),
|
||||
#[doc(hidden)]
|
||||
Mock(Config),
|
||||
}
|
||||
|
||||
impl fmt::Debug for ConnectionFuture {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}({:?}, ...)",
|
||||
match *self {
|
||||
ConnectionFuture::Unsecured(_, _) => "ConnectionFuture::Unsecured",
|
||||
ConnectionFuture::Secured(_, _) => "ConnectionFuture::Secured",
|
||||
ConnectionFuture::Mock(_) => "ConnectionFuture::Mock",
|
||||
},
|
||||
match *self {
|
||||
ConnectionFuture::Unsecured(ref cfg, _) |
|
||||
ConnectionFuture::Secured(ref cfg, _) |
|
||||
ConnectionFuture::Mock(ref cfg) => cfg,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for ConnectionFuture {
|
||||
type Item = Connection;
|
||||
type Error = error::IrcError;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
match *self {
|
||||
ConnectionFuture::Unsecured(ref config, ref mut inner) => {
|
||||
let stream = try_ready!(inner.poll());
|
||||
let framed = IrcCodec::new(config.encoding())?.framed(stream);
|
||||
let transport = IrcTransport::new(&config, framed);
|
||||
|
||||
Ok(Async::Ready(Connection::Unsecured(transport)))
|
||||
}
|
||||
ConnectionFuture::Secured(ref config, ref mut inner) => {
|
||||
let stream = try_ready!(inner.poll());
|
||||
let framed = IrcCodec::new(config.encoding())?.framed(stream);
|
||||
let transport = IrcTransport::new(&config, framed);
|
||||
|
||||
Ok(Async::Ready(Connection::Secured(transport)))
|
||||
}
|
||||
ConnectionFuture::Mock(ref config) => {
|
||||
let enc: error::Result<_> = encoding_from_whatwg_label(
|
||||
config.encoding()
|
||||
).ok_or_else(|| error::IrcError::UnknownCodec {
|
||||
codec: config.encoding().to_owned(),
|
||||
});
|
||||
let encoding = enc?;
|
||||
let init_str = config.mock_initial_value();
|
||||
let initial: error::Result<_> = {
|
||||
encoding.encode(init_str, EncoderTrap::Replace).map_err(|data| {
|
||||
error::IrcError::CodecFailed {
|
||||
codec: encoding.name(),
|
||||
data: data.into_owned(),
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let stream = MockStream::new(&initial?);
|
||||
let framed = IrcCodec::new(config.encoding())?.framed(stream);
|
||||
let transport = IrcTransport::new(&config, framed);
|
||||
|
||||
Ok(Async::Ready(Connection::Mock(Logged::wrap(transport))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// Creates a new `Connection` using the specified `Config`
|
||||
pub fn new(config: Config) -> error::Result<ConnectionFuture> {
|
||||
pub(crate) async fn new(
|
||||
config: &Config,
|
||||
tx: UnboundedSender<Message>,
|
||||
) -> error::Result<Connection> {
|
||||
if config.use_mock_connection() {
|
||||
Ok(ConnectionFuture::Mock(config))
|
||||
} else if config.use_ssl() {
|
||||
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 = crate::client::mock::MockStream::new(&initial);
|
||||
let framed = IrcCodec::new(config.encoding())?.framed(stream);
|
||||
let transport = Transport::new(&config, framed, tx);
|
||||
return Ok(Connection::Mock(Logged::wrap(transport)));
|
||||
}
|
||||
|
||||
if config.use_ssl() {
|
||||
let domain = format!("{}", config.server()?);
|
||||
info!("Connecting via SSL to {}.", domain);
|
||||
log::info!("Connecting via SSL to {}.", domain);
|
||||
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);
|
||||
info!("Added {} to trusted certificates.", cert_path);
|
||||
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)?;
|
||||
|
@ -145,25 +96,27 @@ impl Connection {
|
|||
file.read_to_end(&mut client_cert_data)?;
|
||||
let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?;
|
||||
builder.identity(pkcs12_archive);
|
||||
info!("Using {} for client certificate authentication.", client_cert_path);
|
||||
log::info!(
|
||||
"Using {} for client certificate authentication.",
|
||||
client_cert_path
|
||||
);
|
||||
}
|
||||
let connector: tokio_tls::TlsConnector = builder.build()?.into();
|
||||
let stream = Box::new(TcpStream::connect(&config.socket_addr()?).map_err(|e| {
|
||||
let res: error::IrcError = e.into();
|
||||
res
|
||||
}).and_then(move |socket| {
|
||||
connector.connect(&domain, socket).map_err(
|
||||
|e| e.into(),
|
||||
)
|
||||
}));
|
||||
Ok(ConnectionFuture::Secured(config, stream))
|
||||
|
||||
let socket = TcpStream::connect(&config.socket_addr()?).await?;
|
||||
let stream = connector.connect(&domain, socket).await?;
|
||||
let framed = IrcCodec::new(config.encoding())?.framed(stream);
|
||||
let transport = Transport::new(&config, framed, tx);
|
||||
|
||||
Ok(Connection::Secured(transport))
|
||||
} else {
|
||||
info!("Connecting to {}.", config.server()?);
|
||||
log::info!("Connecting to {}.", config.server()?);
|
||||
let addr = config.socket_addr()?;
|
||||
Ok(ConnectionFuture::Unsecured(
|
||||
config,
|
||||
TcpStream::connect(&addr),
|
||||
))
|
||||
let stream = TcpStream::connect(&addr).await?;
|
||||
let framed = IrcCodec::new(config.encoding())?.framed(stream);
|
||||
let transport = Transport::new(&config, framed, tx);
|
||||
|
||||
Ok(Connection::Unsecured(transport))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,35 +131,49 @@ impl Connection {
|
|||
}
|
||||
|
||||
impl Stream for Connection {
|
||||
type Item = Message;
|
||||
type Error = error::IrcError;
|
||||
type Item = error::Result<Message>;
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||
match *self {
|
||||
Connection::Unsecured(ref mut inner) => inner.poll(),
|
||||
Connection::Secured(ref mut inner) => inner.poll(),
|
||||
Connection::Mock(ref mut inner) => inner.poll(),
|
||||
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),
|
||||
Connection::Secured(inner) => Pin::new(inner).poll_next(cx),
|
||||
Connection::Mock(inner) => Pin::new(inner).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for Connection {
|
||||
type SinkItem = Message;
|
||||
type SinkError = error::IrcError;
|
||||
impl Sink<Message> for Connection {
|
||||
type Error = error::Error;
|
||||
|
||||
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
|
||||
match *self {
|
||||
Connection::Unsecured(ref mut inner) => inner.start_send(item),
|
||||
Connection::Secured(ref mut inner) => inner.start_send(item),
|
||||
Connection::Mock(ref mut inner) => inner.start_send(item),
|
||||
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),
|
||||
Connection::Secured(inner) => Pin::new(inner).poll_ready(cx),
|
||||
Connection::Mock(inner) => Pin::new(inner).poll_ready(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
|
||||
match *self {
|
||||
Connection::Unsecured(ref mut inner) => inner.poll_complete(),
|
||||
Connection::Secured(ref mut inner) => inner.poll_complete(),
|
||||
Connection::Mock(ref mut inner) => inner.poll_complete(),
|
||||
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),
|
||||
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),
|
||||
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),
|
||||
Connection::Secured(inner) => Pin::new(inner).poll_close(cx),
|
||||
Connection::Mock(inner) => Pin::new(inner).poll_close(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
src/client/data/client_config.json
Normal file
19
src/client/data/client_config.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"owners": [
|
||||
"test"
|
||||
],
|
||||
"nickname": "test",
|
||||
"username": "test",
|
||||
"realname": "test",
|
||||
"password": "",
|
||||
"server": "irc.test.net",
|
||||
"port": 6667,
|
||||
"use_ssl": false,
|
||||
"encoding": "UTF-8",
|
||||
"channels": [
|
||||
"#test",
|
||||
"#test2"
|
||||
],
|
||||
"umodes": "+BR",
|
||||
"options": {}
|
||||
}
|
13
src/client/data/client_config.toml
Normal file
13
src/client/data/client_config.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
owners = ["test"]
|
||||
nickname = "test"
|
||||
username = "test"
|
||||
realname = "test"
|
||||
server = "irc.test.net"
|
||||
port = 6667
|
||||
password = ""
|
||||
use_ssl = false
|
||||
encoding = "UTF-8"
|
||||
channels = ["#test", "#test2"]
|
||||
umodes = "+BR"
|
||||
|
||||
[options]
|
16
src/client/data/client_config.yaml
Normal file
16
src/client/data/client_config.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
owners:
|
||||
- test
|
||||
nickname: test
|
||||
username: test
|
||||
realname: test
|
||||
server: irc.test.net
|
||||
port: 6667
|
||||
password: ""
|
||||
use_ssl: false
|
||||
encoding: UTF-8
|
||||
channels:
|
||||
- "#test"
|
||||
- "#test2"
|
||||
umodes: +BR
|
||||
options: {}
|
|
@ -1,10 +1,12 @@
|
|||
//! JSON configuration files using serde
|
||||
use std::borrow::ToOwned;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::{Path, PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::prelude::*,
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
use serde_json;
|
||||
|
@ -13,10 +15,10 @@ use serde_yaml;
|
|||
#[cfg(feature = "toml")]
|
||||
use toml;
|
||||
|
||||
use crate::error::Error::InvalidConfig;
|
||||
#[cfg(feature = "toml")]
|
||||
use error::TomlError;
|
||||
use error::{ConfigError, Result};
|
||||
use error::IrcError::InvalidConfig;
|
||||
use crate::error::TomlError;
|
||||
use crate::error::{ConfigError, Result};
|
||||
|
||||
/// Configuration for IRC clients.
|
||||
///
|
||||
|
@ -153,9 +155,10 @@ impl Config {
|
|||
}
|
||||
|
||||
fn path(&self) -> String {
|
||||
self.path.as_ref().map(|buf| buf.to_string_lossy().into_owned()).unwrap_or_else(|| {
|
||||
"<none>".to_owned()
|
||||
})
|
||||
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
|
||||
|
@ -182,68 +185,54 @@ impl Config {
|
|||
}),
|
||||
};
|
||||
|
||||
res.map(|config| {
|
||||
config.with_path(path)
|
||||
})
|
||||
res.map(|config| config.with_path(path))
|
||||
}
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
fn load_json<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
|
||||
serde_json::from_str(&data[..]).map_err(|e| {
|
||||
InvalidConfig {
|
||||
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"))]
|
||||
fn load_json<P: AsRef<Path>>(path: &P, _: &str) -> Result<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"
|
||||
}
|
||||
cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
fn load_toml<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
|
||||
toml::from_str(&data[..]).map_err(|e| {
|
||||
InvalidConfig {
|
||||
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"))]
|
||||
fn load_toml<P: AsRef<Path>>(path: &P, _: &str) -> Result<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"
|
||||
}
|
||||
cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "yaml")]
|
||||
fn load_yaml<P: AsRef<Path>>(path: &P, data: &str) -> Result<Config> {
|
||||
serde_yaml::from_str(&data[..]).map_err(|e| {
|
||||
InvalidConfig {
|
||||
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"))]
|
||||
fn load_yaml<P: AsRef<Path>>(path: &P, _: &str) -> Result<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"
|
||||
}
|
||||
cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -257,16 +246,20 @@ impl Config {
|
|||
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 {
|
||||
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 {
|
||||
})
|
||||
}
|
||||
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());
|
||||
|
@ -275,11 +268,9 @@ impl Config {
|
|||
|
||||
#[cfg(feature = "json")]
|
||||
fn save_json<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
|
||||
serde_json::to_string(self).map_err(|e| {
|
||||
InvalidConfig {
|
||||
serde_json::to_string(self).map_err(|e| InvalidConfig {
|
||||
path: path.as_ref().to_string_lossy().into_owned(),
|
||||
cause: ConfigError::InvalidJson(e),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -287,19 +278,15 @@ impl 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"
|
||||
}
|
||||
cause: ConfigError::ConfigFormatDisabled { format: "JSON" },
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "toml")]
|
||||
fn save_toml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
|
||||
toml::to_string(self).map_err(|e| {
|
||||
InvalidConfig {
|
||||
toml::to_string(self).map_err(|e| InvalidConfig {
|
||||
path: path.as_ref().to_string_lossy().into_owned(),
|
||||
cause: ConfigError::InvalidToml(TomlError::Write(e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -307,19 +294,15 @@ impl 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"
|
||||
}
|
||||
cause: ConfigError::ConfigFormatDisabled { format: "TOML" },
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "yaml")]
|
||||
fn save_yaml<P: AsRef<Path>>(&self, path: &P) -> Result<String> {
|
||||
serde_yaml::to_string(self).map_err(|e| {
|
||||
InvalidConfig {
|
||||
serde_yaml::to_string(self).map_err(|e| InvalidConfig {
|
||||
path: path.as_ref().to_string_lossy().into_owned(),
|
||||
cause: ConfigError::InvalidYaml(e),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -327,9 +310,7 @@ impl 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"
|
||||
}
|
||||
cause: ConfigError::ConfigFormatDisabled { format: "YAML" },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -343,11 +324,12 @@ impl Config {
|
|||
|
||||
/// Gets the nickname specified in the configuration.
|
||||
pub fn nickname(&self) -> Result<&str> {
|
||||
self.nickname.as_ref().map(|s| &s[..]).ok_or_else(|| {
|
||||
InvalidConfig {
|
||||
self.nickname
|
||||
.as_ref()
|
||||
.map(|s| &s[..])
|
||||
.ok_or_else(|| InvalidConfig {
|
||||
path: self.path(),
|
||||
cause: ConfigError::NicknameNotSpecified,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -360,48 +342,52 @@ impl Config {
|
|||
/// Gets the alternate nicknames specified in the configuration.
|
||||
/// This defaults to an empty vector when not specified.
|
||||
pub fn alternate_nicknames(&self) -> Vec<&str> {
|
||||
self.alt_nicks.as_ref().map_or(vec![], |v| {
|
||||
v.iter().map(|s| &s[..]).collect()
|
||||
})
|
||||
self.alt_nicks
|
||||
.as_ref()
|
||||
.map_or(vec![], |v| v.iter().map(|s| &s[..]).collect())
|
||||
}
|
||||
|
||||
|
||||
/// 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)
|
||||
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)
|
||||
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(|s| &s[..]).ok_or_else(|| {
|
||||
InvalidConfig {
|
||||
self.server
|
||||
.as_ref()
|
||||
.map(|s| &s[..])
|
||||
.ok_or_else(|| InvalidConfig {
|
||||
path: self.path(),
|
||||
cause: ConfigError::ServerNotSpecified,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the port of the server specified in the configuration.
|
||||
/// This defaults to 6667 (or 6697 if use_ssl is specified as true) when not specified.
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port.as_ref().cloned().unwrap_or(if self.use_ssl() {
|
||||
6697
|
||||
} else {
|
||||
6667
|
||||
})
|
||||
self.port
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or(if self.use_ssl() { 6697 } else { 6667 })
|
||||
}
|
||||
|
||||
/// Gets the server and port as a `SocketAddr`.
|
||||
/// This panics when server is not specified or the address is malformed.
|
||||
pub fn socket_addr(&self) -> Result<SocketAddr> {
|
||||
format!("{}:{}", self.server()?, self.port()).to_socket_addrs()
|
||||
format!("{}:{}", self.server()?, self.port())
|
||||
.to_socket_addrs()
|
||||
.map(|mut i| i.next().unwrap())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
@ -442,17 +428,16 @@ impl Config {
|
|||
/// Gets the channels to join upon connection.
|
||||
/// This defaults to an empty vector if it's not specified.
|
||||
pub fn channels(&self) -> Vec<&str> {
|
||||
self.channels.as_ref().map_or(vec![], |v| {
|
||||
v.iter().map(|s| &s[..]).collect()
|
||||
})
|
||||
self.channels
|
||||
.as_ref()
|
||||
.map_or(vec![], |v| v.iter().map(|s| &s[..]).collect())
|
||||
}
|
||||
|
||||
|
||||
/// 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.as_ref().and_then(|m| {
|
||||
m.get(&chan.to_owned()).map(|s| &s[..])
|
||||
})
|
||||
self.channel_keys
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(&chan.to_owned()).map(|s| &s[..]))
|
||||
}
|
||||
|
||||
/// Gets the user modes to set on connect specified in the configuration.
|
||||
|
@ -471,16 +456,15 @@ impl Config {
|
|||
/// 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(::VERSION_STR, |s| &s)
|
||||
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",
|
||||
|s| &s[..],
|
||||
)
|
||||
self.source
|
||||
.as_ref()
|
||||
.map_or("https://github.com/aatxe/irc", |s| &s[..])
|
||||
}
|
||||
|
||||
/// Gets the amount of time in seconds for the interval at which the client pings the server.
|
||||
|
@ -522,16 +506,16 @@ impl Config {
|
|||
/// Gets the NickServ command sequence to recover a nickname.
|
||||
/// This defaults to `["GHOST"]` when not specified.
|
||||
pub fn ghost_sequence(&self) -> Vec<&str> {
|
||||
self.ghost_sequence.as_ref().map_or(vec!["GHOST"], |v| {
|
||||
v.iter().map(|s| &s[..]).collect()
|
||||
})
|
||||
self.ghost_sequence
|
||||
.as_ref()
|
||||
.map_or(vec!["GHOST"], |v| v.iter().map(|s| &s[..]).collect())
|
||||
}
|
||||
|
||||
/// Looks up the specified string in the options map.
|
||||
pub fn get_option(&self, option: &str) -> Option<&str> {
|
||||
self.options.as_ref().and_then(|o| {
|
||||
o.get(&option.to_owned()).map(|s| &s[..])
|
||||
})
|
||||
self.options
|
||||
.as_ref()
|
||||
.and_then(|o| o.get(&option.to_owned()).map(|s| &s[..]))
|
||||
}
|
||||
|
||||
/// Gets whether or not to use a mock connection for testing.
|
||||
|
@ -550,12 +534,8 @@ impl Config {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
use std::default::Default;
|
||||
#[cfg(feature = "json")]
|
||||
use std::path::Path;
|
||||
|
||||
use super::Config;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[allow(unused)]
|
||||
fn test_config() -> Config {
|
||||
|
@ -594,24 +574,6 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "json")]
|
||||
fn load_from_json() {
|
||||
assert_eq!(Config::load("client_config.json").unwrap(), test_config().with_path("client_config.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "toml")]
|
||||
fn load_from_toml() {
|
||||
assert_eq!(Config::load("client_config.toml").unwrap(), test_config().with_path("client_config.toml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "yaml")]
|
||||
fn load_from_yaml() {
|
||||
assert_eq!(Config::load("client_config.yaml").unwrap(), test_config().with_path("client_config.yaml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_owner() {
|
||||
let cfg = Config {
|
||||
|
@ -636,4 +598,37 @@ mod test {
|
|||
assert_eq!(cfg.get_option("testing"), Some("test"));
|
||||
assert_eq!(cfg.get_option("not"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "json")]
|
||||
fn load_from_json() -> Result<(), failure::Error> {
|
||||
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")]
|
||||
fn load_from_toml() -> Result<(), failure::Error> {
|
||||
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")]
|
||||
fn load_from_yaml() -> Result<(), failure::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Data related to IRC functionality.
|
||||
|
||||
pub use client::data::config::Config;
|
||||
pub use client::data::user::{AccessLevel, User};
|
||||
pub use crate::client::data::config::Config;
|
||||
pub use crate::client::data::user::{AccessLevel, User};
|
||||
|
||||
pub mod config;
|
||||
pub mod user;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
//! Data for tracking user information.
|
||||
use std::borrow::ToOwned;
|
||||
use std::cmp::Ordering;
|
||||
use std::cmp::Ordering::{Less, Equal, Greater};
|
||||
use std::cmp::Ordering::{Equal, Greater, Less};
|
||||
use std::str::FromStr;
|
||||
|
||||
use proto::{Mode, ChannelMode};
|
||||
use crate::proto::{ChannelMode, Mode};
|
||||
|
||||
/// IRC User data.
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -124,8 +124,9 @@ impl User {
|
|||
|
||||
impl PartialEq for User {
|
||||
fn eq(&self, other: &User) -> bool {
|
||||
self.nickname == other.nickname && self.username == other.username &&
|
||||
self.hostname == other.hostname
|
||||
self.nickname == other.nickname
|
||||
&& self.username == other.username
|
||||
&& self.hostname == other.hostname
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,7 +209,9 @@ struct AccessLevelIterator {
|
|||
|
||||
impl AccessLevelIterator {
|
||||
pub fn new(value: &str) -> AccessLevelIterator {
|
||||
AccessLevelIterator { value: value.to_owned() }
|
||||
AccessLevelIterator {
|
||||
value: value.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,10 +228,10 @@ impl Iterator for AccessLevelIterator {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{AccessLevel, User};
|
||||
use super::AccessLevel::*;
|
||||
use proto::ChannelMode as M;
|
||||
use proto::Mode::*;
|
||||
use super::{AccessLevel, User};
|
||||
use crate::proto::ChannelMode as M;
|
||||
use crate::proto::Mode::*;
|
||||
|
||||
#[test]
|
||||
fn parse_access_level() {
|
||||
|
@ -269,7 +272,6 @@ mod test {
|
|||
assert_eq!(user, exp);
|
||||
assert_eq!(user.highest_access_level, exp.highest_access_level);
|
||||
assert_eq!(user.access_levels, exp.access_levels);
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,687 +0,0 @@
|
|||
//! Utilities and shortcuts for working with IRC servers.
|
||||
//!
|
||||
//! This module provides the [`ClientExt`](trait.ClientExt.html) trait which is the idiomatic way of
|
||||
//! sending messages to an IRC server. This trait is automatically implemented for everything that
|
||||
//! implements [`Client`](../trait.Client.html) and is designed to provide important functionality
|
||||
//! without clutter.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! Using these APIs, we can connect to a server and send a one-off message (in this case,
|
||||
//! identifying with the server).
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # extern crate irc;
|
||||
//! use irc::client::prelude::{IrcClient, ClientExt};
|
||||
//!
|
||||
//! # fn main() {
|
||||
//! let server = IrcClient::new("config.toml").unwrap();
|
||||
//! // identify and send_privmsg both come from `ClientExt`
|
||||
//! server.identify().unwrap();
|
||||
//! server.send_privmsg("#example", "Hello, world!").unwrap();
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! `ClientExt::identify` also plays an important role in performing IRCv3 capability negotiations.
|
||||
//! In particular, calling `identify` will close the negotiations (and otherwise indicate IRCv3
|
||||
//! compatibility). This means that all IRCv3 capability requests should be performed before calling
|
||||
//! `identify`. For example:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # extern crate irc;
|
||||
//! # use irc::client::prelude::*;
|
||||
//! # fn main() {
|
||||
//! # let server = IrcClient::new("config.toml").unwrap();
|
||||
//! server.send_cap_req(&[Capability::MultiPrefix, Capability::UserhostInNames]).unwrap();
|
||||
//! server.identify().unwrap();
|
||||
//! # }
|
||||
//! ```
|
||||
use std::string::ToString;
|
||||
|
||||
#[cfg(feature = "ctcp")]
|
||||
use chrono::prelude::*;
|
||||
|
||||
use error::Result;
|
||||
use proto::{Capability, Command, Mode, NegotiationVersion};
|
||||
use proto::command::CapSubCommand::{END, LS, REQ};
|
||||
use proto::command::Command::*;
|
||||
use proto::mode::ModeType;
|
||||
use client::Client;
|
||||
|
||||
/// Idiomatic extensions for sending messages to an IRC server as a [`Client`](../trait.Client.html).
|
||||
pub trait ClientExt: Client {
|
||||
/// Sends a request for a list of server capabilities for a specific IRCv3 version.
|
||||
fn send_cap_ls(&self, version: NegotiationVersion) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.send(Command::CAP(
|
||||
None,
|
||||
LS,
|
||||
match version {
|
||||
NegotiationVersion::V301 => None,
|
||||
NegotiationVersion::V302 => Some("302".to_owned()),
|
||||
},
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
/// Sends an IRCv3 capabilities request for the specified extensions.
|
||||
fn send_cap_req(&self, extensions: &[Capability]) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let append = |mut s: String, c| {
|
||||
s.push_str(c);
|
||||
s.push(' ');
|
||||
s
|
||||
};
|
||||
let mut exts = extensions.iter().map(|c| c.as_ref()).fold(
|
||||
String::new(),
|
||||
append,
|
||||
);
|
||||
let len = exts.len() - 1;
|
||||
exts.truncate(len);
|
||||
self.send(CAP(None, REQ, None, Some(exts)))
|
||||
}
|
||||
|
||||
/// Sends a CAP END, NICK and USER to identify.
|
||||
fn identify(&self) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// Send a CAP END to signify that we're IRCv3-compliant (and to end negotiations!).
|
||||
self.send(CAP(None, END, None, None))?;
|
||||
if self.config().password() != "" {
|
||||
self.send(PASS(self.config().password().to_owned()))?;
|
||||
}
|
||||
self.send(NICK(self.config().nickname()?.to_owned()))?;
|
||||
self.send(USER(
|
||||
self.config().username().to_owned(),
|
||||
"0".to_owned(),
|
||||
self.config().real_name().to_owned(),
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a SASL AUTHENTICATE message with the specified data.
|
||||
fn send_sasl<S: ToString>(&self, data: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.send(AUTHENTICATE(data.to_string()))
|
||||
}
|
||||
|
||||
/// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism.
|
||||
fn send_sasl_plain(&self) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.send_sasl("PLAIN")
|
||||
}
|
||||
|
||||
|
||||
/// Sends a SASL AUTHENTICATE request to use the EXTERNAL mechanism.
|
||||
fn send_sasl_external(&self) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.send_sasl("EXTERNAL")
|
||||
}
|
||||
|
||||
/// Sends a SASL AUTHENTICATE request to abort authentication.
|
||||
fn send_sasl_abort(&self) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.send_sasl("*")
|
||||
}
|
||||
|
||||
/// Sends a PONG with the specified message.
|
||||
fn send_pong<S>(&self, msg: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
self.send(PONG(msg.to_string(), None))
|
||||
}
|
||||
|
||||
/// Joins the specified channel or chanlist.
|
||||
fn send_join<S>(&self, chanlist: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
self.send(JOIN(chanlist.to_string(), None, None))
|
||||
}
|
||||
|
||||
/// Joins the specified channel or chanlist using the specified key or keylist.
|
||||
fn send_join_with_keys<S1, S2>(&self, chanlist: &str, keylist: &str) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
self.send(JOIN(chanlist.to_string(), Some(keylist.to_string()), None))
|
||||
}
|
||||
|
||||
/// Parts the specified channel or chanlist.
|
||||
fn send_part<S>(&self, chanlist: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
self.send(PART(chanlist.to_string(), None))
|
||||
}
|
||||
|
||||
/// Attempts to oper up using the specified username and password.
|
||||
fn send_oper<S1, S2>(&self, username: S1, password: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
self.send(OPER(username.to_string(), password.to_string()))
|
||||
}
|
||||
|
||||
/// Sends a message to the specified target. If the message contains IRC newlines (`\r\n`), it
|
||||
/// will automatically be split and sent as multiple separate `PRIVMSG`s to the specified
|
||||
/// target. If you absolutely must avoid this behavior, you can do
|
||||
/// `client.send(PRIVMSG(target, message))` directly.
|
||||
fn send_privmsg<S1, S2>(&self, target: S1, message: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
let message = message.to_string();
|
||||
for line in message.split("\r\n") {
|
||||
self.send(PRIVMSG(target.to_string(), line.to_string()))?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a notice to the specified target.
|
||||
fn send_notice<S1, S2>(&self, target: S1, message: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
let message = message.to_string();
|
||||
for line in message.split("\r\n") {
|
||||
self.send(NOTICE(target.to_string(), line.to_string()))?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the topic of a channel or requests the current one.
|
||||
/// If `topic` is an empty string, it won't be included in the message.
|
||||
fn send_topic<S1, S2>(&self, channel: S1, topic: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
let topic = topic.to_string();
|
||||
self.send(TOPIC(
|
||||
channel.to_string(),
|
||||
if topic.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(topic)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Kills the target with the provided message.
|
||||
fn send_kill<S1, S2>(&self, target: S1, message: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
self.send(KILL(target.to_string(), message.to_string()))
|
||||
}
|
||||
|
||||
/// Kicks the listed nicknames from the listed channels with a comment.
|
||||
/// If `message` is an empty string, it won't be included in the message.
|
||||
fn send_kick<S1, S2, S3>(&self, chanlist: S1, nicklist: S2, message: S3) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
S3: ToString,
|
||||
{
|
||||
let message = message.to_string();
|
||||
self.send(KICK(
|
||||
chanlist.to_string(),
|
||||
nicklist.to_string(),
|
||||
if message.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(message)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Changes the modes for the specified target.
|
||||
fn send_mode<S, T>(&self, target: S, modes: &[Mode<T>]) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
T: ModeType,
|
||||
{
|
||||
self.send(T::mode(&target.to_string(), modes))
|
||||
}
|
||||
|
||||
/// Changes the mode of the target by force.
|
||||
/// If `modeparams` is an empty string, it won't be included in the message.
|
||||
fn send_samode<S1, S2, S3>(&self, target: S1, mode: S2, modeparams: S3) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
S3: ToString,
|
||||
{
|
||||
let modeparams = modeparams.to_string();
|
||||
self.send(SAMODE(
|
||||
target.to_string(),
|
||||
mode.to_string(),
|
||||
if modeparams.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(modeparams)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Forces a user to change from the old nickname to the new nickname.
|
||||
fn send_sanick<S1, S2>(&self, old_nick: S1, new_nick: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
self.send(SANICK(old_nick.to_string(), new_nick.to_string()))
|
||||
}
|
||||
|
||||
/// Invites a user to the specified channel.
|
||||
fn send_invite<S1, S2>(&self, nick: S1, chan: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
self.send(INVITE(nick.to_string(), chan.to_string()))
|
||||
}
|
||||
|
||||
/// Quits the server entirely with a message.
|
||||
/// This defaults to `Powered by Rust.` if none is specified.
|
||||
fn send_quit<S>(&self, msg: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
let msg = msg.to_string();
|
||||
self.send(QUIT(Some(if msg.is_empty() {
|
||||
"Powered by Rust.".to_string()
|
||||
} else {
|
||||
msg
|
||||
})))
|
||||
}
|
||||
|
||||
/// Sends a CTCP-escaped message to the specified target.
|
||||
/// This requires the CTCP feature to be enabled.
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_ctcp<S1, S2>(&self, target: S1, msg: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
self.send_privmsg(target, &format!("\u{001}{}\u{001}", msg.to_string())[..])
|
||||
}
|
||||
|
||||
/// Sends an action command to the specified target.
|
||||
/// This requires the CTCP feature to be enabled.
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_action<S1, S2>(&self, target: S1, msg: S2) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S1: ToString,
|
||||
S2: ToString,
|
||||
{
|
||||
self.send_ctcp(target, &format!("ACTION {}", msg.to_string())[..])
|
||||
}
|
||||
|
||||
/// Sends a finger request to the specified target.
|
||||
/// This requires the CTCP feature to be enabled.
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_finger<S: ToString>(&self, target: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
self.send_ctcp(target, "FINGER")
|
||||
}
|
||||
|
||||
/// Sends a version request to the specified target.
|
||||
/// This requires the CTCP feature to be enabled.
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_version<S>(&self, target: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
self.send_ctcp(target, "VERSION")
|
||||
}
|
||||
|
||||
/// Sends a source request to the specified target.
|
||||
/// This requires the CTCP feature to be enabled.
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_source<S>(&self, target: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
self.send_ctcp(target, "SOURCE")
|
||||
}
|
||||
|
||||
/// Sends a user info request to the specified target.
|
||||
/// This requires the CTCP feature to be enabled.
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_user_info<S>(&self, target: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
self.send_ctcp(target, "USERINFO")
|
||||
}
|
||||
|
||||
/// Sends a finger request to the specified target.
|
||||
/// This requires the CTCP feature to be enabled.
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_ctcp_ping<S>(&self, target: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
let time = Local::now();
|
||||
self.send_ctcp(target, &format!("PING {}", time.timestamp())[..])
|
||||
}
|
||||
|
||||
/// Sends a time request to the specified target.
|
||||
/// This requires the CTCP feature to be enabled.
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_time<S>(&self, target: S) -> Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
S: ToString,
|
||||
{
|
||||
self.send_ctcp(target, "TIME")
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ClientExt for C where C: Client {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ClientExt;
|
||||
use client::data::Config;
|
||||
use client::IrcClient;
|
||||
use client::test::{get_client_value, test_config};
|
||||
use proto::{ChannelMode, Mode};
|
||||
|
||||
#[test]
|
||||
fn identify() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.identify().unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"CAP END\r\nNICK :test\r\n\
|
||||
USER test 0 * :test\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identify_with_password() {
|
||||
let client = IrcClient::from_config(Config {
|
||||
nickname: Some(format!("test")),
|
||||
password: Some(format!("password")),
|
||||
..test_config()
|
||||
}).unwrap();
|
||||
client.identify().unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"CAP END\r\nPASS :password\r\nNICK :test\r\n\
|
||||
USER test 0 * :test\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_pong() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_pong("irc.test.net").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "PONG :irc.test.net\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_join() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_join("#test,#test2,#test3").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"JOIN #test,#test2,#test3\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_part() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_part("#test").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "PART #test\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_oper() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_oper("test", "test").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "OPER test :test\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_privmsg() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_privmsg("#test", "Hi, everybody!").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"PRIVMSG #test :Hi, everybody!\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_notice() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_notice("#test", "Hi, everybody!").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"NOTICE #test :Hi, everybody!\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_topic_no_topic() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_topic("#test", "").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "TOPIC #test\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_topic() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_topic("#test", "Testing stuff.").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"TOPIC #test :Testing stuff.\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_kill() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_kill("test", "Testing kills.").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"KILL test :Testing kills.\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_kick_no_message() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_kick("#test", "test", "").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "KICK #test test\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_kick() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_kick("#test", "test", "Testing kicks.").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"KICK #test test :Testing kicks.\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_mode_no_modeparams() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "MODE #test +i\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_mode() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))])
|
||||
.unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "MODE #test +o test\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_samode_no_modeparams() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_samode("#test", "+i", "").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "SAMODE #test +i\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_samode() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_samode("#test", "+o", "test").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "SAMODE #test +o test\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_sanick() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_sanick("test", "test2").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "SANICK test test2\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_invite() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_invite("test", "#test").unwrap();
|
||||
assert_eq!(&get_client_value(client)[..], "INVITE test #test\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_ctcp() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_ctcp("test", "MESSAGE").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"PRIVMSG test :\u{001}MESSAGE\u{001}\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_action() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_action("test", "tests.").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_finger() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_finger("test").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"PRIVMSG test :\u{001}FINGER\u{001}\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_version() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_version("test").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"PRIVMSG test :\u{001}VERSION\u{001}\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_source() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_source("test").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"PRIVMSG test :\u{001}SOURCE\u{001}\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_user_info() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_user_info("test").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"PRIVMSG test :\u{001}USERINFO\u{001}\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_ctcp_ping() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_ctcp_ping("test").unwrap();
|
||||
let val = get_client_value(client);
|
||||
println!("{}", val);
|
||||
assert!(val.starts_with("PRIVMSG test :\u{001}PING "));
|
||||
assert!(val.ends_with("\u{001}\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ctcp")]
|
||||
fn send_time() {
|
||||
let client = IrcClient::from_config(test_config()).unwrap();
|
||||
client.send_time("test").unwrap();
|
||||
assert_eq!(
|
||||
&get_client_value(client)[..],
|
||||
"PRIVMSG test :\u{001}TIME\u{001}\r\n"
|
||||
);
|
||||
}
|
||||
}
|
66
src/client/mock.rs
Normal file
66
src/client/mock.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use std::{
|
||||
io::{self, Cursor, Read, Write},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
/// A fake stream for testing network applications backed by buffers.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockStream {
|
||||
written: Cursor<Vec<u8>>,
|
||||
received: Cursor<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl MockStream {
|
||||
/// Creates a new mock stream with nothing to read.
|
||||
pub fn empty() -> MockStream {
|
||||
MockStream::new(&[])
|
||||
}
|
||||
|
||||
/// Creates a new mock stream with the specified bytes to read.
|
||||
pub fn new(initial: &[u8]) -> MockStream {
|
||||
MockStream {
|
||||
written: Cursor::new(vec![]),
|
||||
received: Cursor::new(initial.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a slice of bytes representing the data that has been written.
|
||||
pub fn written(&self) -> &[u8] {
|
||||
self.written.get_ref()
|
||||
}
|
||||
|
||||
/// Gets a slice of bytes representing the data that has been received.
|
||||
pub fn received(&self) -> &[u8] {
|
||||
self.received.get_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for MockStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Poll::Ready(self.as_mut().received.read(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for MockStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, io::Error>> {
|
||||
Poll::Ready(self.as_mut().written.write(buf))
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
Poll::Ready(self.as_mut().written.flush())
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
1952
src/client/mod.rs
1952
src/client/mod.rs
File diff suppressed because it is too large
Load diff
|
@ -21,11 +21,10 @@
|
|||
//! 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.
|
||||
|
||||
pub use client::data::Config;
|
||||
pub use client::reactor::IrcReactor;
|
||||
pub use client::{EachIncomingExt, IrcClient, Client};
|
||||
pub use client::ext::ClientExt;
|
||||
pub use proto::{Capability, ChannelExt, Command, Message, Prefix, NegotiationVersion, Response};
|
||||
pub use proto::{ChannelMode, Mode, UserMode};
|
||||
|
||||
pub use futures::{Future, Stream};
|
||||
pub use crate::{
|
||||
client::{data::Config, Client, Sender},
|
||||
proto::{
|
||||
Capability, ChannelExt, ChannelMode, Command, Message, Mode, NegotiationVersion, Prefix,
|
||||
Response, UserMode,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
//! A system for creating and managing IRC client connections.
|
||||
//!
|
||||
//! This API provides the ability to create and manage multiple IRC clients that can run on the same
|
||||
//! thread through the use of a shared event loop. It can also be used to encapsulate the dependency
|
||||
//! on `tokio` and `futures` in the use of `IrcClient::new_future`. This means that knowledge of
|
||||
//! those libraries should be unnecessary for the average user. Nevertheless, this API also provides
|
||||
//! some escape hatches that let advanced users take further advantage of these dependencies.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # extern crate irc;
|
||||
//! # use std::default::Default;
|
||||
//! use irc::client::prelude::*;
|
||||
//! use irc::error;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let config = Config::default();
|
||||
//! let mut reactor = IrcReactor::new().unwrap();
|
||||
//! let client = reactor.prepare_client_and_connect(config).unwrap();
|
||||
//! reactor.register_client_with_handler(client, process_msg);
|
||||
//! reactor.run().unwrap();
|
||||
//! }
|
||||
//! # fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { Ok(()) }
|
||||
//! ```
|
||||
|
||||
use futures::{Future, IntoFuture, Stream};
|
||||
use futures::future;
|
||||
use tokio::runtime::current_thread as tokio_rt;
|
||||
|
||||
use client::data::Config;
|
||||
use client::{IrcClient, IrcClientFuture, PackedIrcClient, Client};
|
||||
use error;
|
||||
use proto::Message;
|
||||
|
||||
/// A thin wrapper over an event loop.
|
||||
///
|
||||
/// An IRC reactor is used to create new IRC clients and to drive the management of all connected
|
||||
/// clients as the application runs. It can be used to run multiple clients on the same thread, as
|
||||
/// well as to get better control over error management in an IRC client.
|
||||
///
|
||||
/// For a full example usage, see [`irc::client::reactor`](./index.html).
|
||||
pub struct IrcReactor {
|
||||
inner: tokio_rt::Runtime,
|
||||
handlers: Vec<Box<Future<Item = (), Error = error::IrcError>>>,
|
||||
}
|
||||
|
||||
impl IrcReactor {
|
||||
/// Creates a new reactor.
|
||||
pub fn new() -> error::Result<IrcReactor> {
|
||||
Ok(IrcReactor {
|
||||
inner: tokio_rt::Runtime::new()?,
|
||||
handlers: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a representation of an IRC client that has not yet attempted to connect. In
|
||||
/// particular, this representation is as a `Future` that when run will produce a connected
|
||||
/// [`IrcClient`](../struct.IrcClient.html).
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # extern crate irc;
|
||||
/// # use std::default::Default;
|
||||
/// # use irc::client::prelude::*;
|
||||
/// # fn main() {
|
||||
/// let future_client = IrcReactor::new().and_then(|mut reactor| {
|
||||
/// reactor.prepare_client(Config::default())
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn prepare_client(&mut self, config: Config) -> error::Result<IrcClientFuture> {
|
||||
IrcClient::new_future(config)
|
||||
}
|
||||
|
||||
/// Runs an [`IrcClientFuture`](../struct.IrcClientFuture.html), such as one from
|
||||
/// `prepare_client` to completion, yielding an [`IrcClient`](../struct.IrcClient.html).
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # extern crate irc;
|
||||
/// # use std::default::Default;
|
||||
/// # use irc::client::prelude::*;
|
||||
/// # fn main() {
|
||||
/// let client = IrcReactor::new().and_then(|mut reactor| {
|
||||
/// reactor.prepare_client(Config::default()).and_then(|future| {
|
||||
/// reactor.connect_client(future)
|
||||
/// })
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn connect_client(&mut self, future: IrcClientFuture) -> error::Result<IrcClient> {
|
||||
self.inner.block_on(future).map(|PackedIrcClient(client, future)| {
|
||||
self.register_future(future);
|
||||
client
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new [`IrcClient`](../struct.IrcClient.html) from the specified configuration,
|
||||
/// connecting immediately. This is guaranteed to be the composition of `prepare_client` and
|
||||
/// `connect_client`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # extern crate irc;
|
||||
/// # use std::default::Default;
|
||||
/// # use irc::client::prelude::*;
|
||||
/// # fn main() {
|
||||
/// let client = IrcReactor::new().and_then(|mut reactor| {
|
||||
/// reactor.prepare_client_and_connect(Config::default())
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn prepare_client_and_connect(&mut self, config: Config) -> error::Result<IrcClient> {
|
||||
self.prepare_client(config).and_then(|future| self.connect_client(future))
|
||||
}
|
||||
|
||||
/// Registers the given client with the specified message handler. The reactor will store this
|
||||
/// setup until the next call to run, where it will be used to process new messages over the
|
||||
/// connection indefinitely (or until failure). As registration is consumed by `run`, subsequent
|
||||
/// calls to run will require new registration.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # extern crate irc;
|
||||
/// # use std::default::Default;
|
||||
/// # use irc::client::prelude::*;
|
||||
/// # fn main() {
|
||||
/// # let config = Config::default();
|
||||
/// let mut reactor = IrcReactor::new().unwrap();
|
||||
/// let client = reactor.prepare_client_and_connect(config).unwrap();
|
||||
/// reactor.register_client_with_handler(client, |client, msg| {
|
||||
/// // Message processing happens here.
|
||||
/// Ok(())
|
||||
/// })
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn register_client_with_handler<F, U>(
|
||||
&mut self, client: IrcClient, mut handler: F
|
||||
) where F: FnMut(&IrcClient, Message) -> U + 'static,
|
||||
U: IntoFuture<Item = (), Error = error::IrcError> + 'static,
|
||||
U::Future: Send {
|
||||
let handle = self.inner.handle().clone();
|
||||
self.handlers.push(Box::new(client.stream().for_each(move |message| {
|
||||
handle.spawn(handler(&client, message).into_future().map_err(|_| (())))?;
|
||||
|
||||
Ok(())
|
||||
})));
|
||||
}
|
||||
|
||||
/// Registers an arbitrary future with this reactor. This is a sort of escape hatch that allows
|
||||
/// you to take more control over what runs on the reactor without requiring you to bring in
|
||||
/// additional knowledge about `tokio`. It is suspected that `register_client_with_handler` will
|
||||
/// be sufficient for most use cases.
|
||||
pub fn register_future<F>(
|
||||
&mut self, future: F
|
||||
) where F: IntoFuture<Item = (), Error = error::IrcError> + 'static {
|
||||
self.handlers.push(Box::new(future.into_future()))
|
||||
}
|
||||
|
||||
/// Returns a handle to the internal event loop. This is a sort of escape hatch that allows you
|
||||
/// to take more control over what runs on the reactor using `tokio`. This can be used for
|
||||
/// sharing this reactor with some elements of other libraries.
|
||||
pub fn inner_handle(&self) -> tokio_rt::Handle {
|
||||
self.inner.handle()
|
||||
}
|
||||
|
||||
/// Consumes all registered handlers and futures, and runs them. When using
|
||||
/// `register_client_with_handler`, this will block indefinitely (until failure occurs) as it
|
||||
/// will simply continue to process new, incoming messages for each client that was registered.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # extern crate irc;
|
||||
/// # use std::default::Default;
|
||||
/// # use irc::client::prelude::*;
|
||||
/// # use irc::error;
|
||||
/// # fn main() {
|
||||
/// # let config = Config::default();
|
||||
/// let mut reactor = IrcReactor::new().unwrap();
|
||||
/// let client = reactor.prepare_client_and_connect(config).unwrap();
|
||||
/// reactor.register_client_with_handler(client, process_msg);
|
||||
/// reactor.run().unwrap();
|
||||
/// # }
|
||||
/// # fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { Ok(()) }
|
||||
/// ```
|
||||
pub fn run(&mut self) -> error::Result<()> {
|
||||
let mut handlers = Vec::new();
|
||||
while let Some(handler) = self.handlers.pop() {
|
||||
handlers.push(handler);
|
||||
}
|
||||
self.inner.block_on(future::join_all(handlers).map(|_| ()))
|
||||
}
|
||||
}
|
|
@ -1,241 +1,208 @@
|
|||
//! 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::collections::VecDeque;
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock, RwLockReadGuard},
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream};
|
||||
use chrono::prelude::*;
|
||||
use tokio_codec::Framed;
|
||||
use tokio_io::{AsyncRead, AsyncWrite};
|
||||
use tokio_timer;
|
||||
use tokio_timer::{Interval, Sleep, Timer};
|
||||
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 error;
|
||||
use client::data::Config;
|
||||
use proto::{Command, IrcCodec, Message};
|
||||
use crate::{
|
||||
client::data::Config,
|
||||
error,
|
||||
proto::{Command, IrcCodec, Message},
|
||||
};
|
||||
|
||||
/// 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 IrcTransport<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
{
|
||||
/// The inner connection framed with an `IrcCodec`.
|
||||
inner: Framed<T, IrcCodec>,
|
||||
/// A timer used in computing windows for message throttling.
|
||||
burst_timer: Timer,
|
||||
/// A queue of tasks used to implement message throttling.
|
||||
rolling_burst_window: VecDeque<Sleep>,
|
||||
/// The amount of time that each window for throttling should last (in seconds).
|
||||
burst_window_length: u64,
|
||||
/// The maximum number of messages that can be sent in each window.
|
||||
max_burst_messages: u64,
|
||||
/// The number of messages sent in the current window.
|
||||
current_burst_messages: u64,
|
||||
/// A timer used to determine when to send the next ping messages to the server.
|
||||
ping_timer: Interval,
|
||||
/// Pinger-based futures helper.
|
||||
struct Pinger {
|
||||
tx: UnboundedSender<Message>,
|
||||
/// The amount of time to wait before timing out from no ping response.
|
||||
ping_timeout: u64,
|
||||
/// The last data sent with a ping.
|
||||
last_ping_data: String,
|
||||
ping_timeout: Duration,
|
||||
/// The instant that the last ping was sent to the server.
|
||||
last_ping_sent: Instant,
|
||||
/// The instant that the last pong was received from the server.
|
||||
last_pong_received: Instant,
|
||||
ping_deadline: Option<Delay>,
|
||||
/// The interval at which to send pings.
|
||||
ping_interval: Interval,
|
||||
}
|
||||
|
||||
impl<T> IrcTransport<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
{
|
||||
/// Creates a new `IrcTransport` from the given IRC stream.
|
||||
pub fn new(config: &Config, inner: Framed<T, IrcCodec>) -> IrcTransport<T> {
|
||||
let timer = tokio_timer::wheel().build();
|
||||
IrcTransport {
|
||||
inner,
|
||||
burst_timer: tokio_timer::wheel().build(),
|
||||
rolling_burst_window: VecDeque::new(),
|
||||
burst_window_length: u64::from(config.burst_window_length()),
|
||||
max_burst_messages: u64::from(config.max_messages_in_burst()),
|
||||
current_burst_messages: 0,
|
||||
ping_timer: timer.interval(Duration::from_secs(u64::from(config.ping_time()))),
|
||||
ping_timeout: u64::from(config.ping_timeout()),
|
||||
last_ping_data: String::new(),
|
||||
last_ping_sent: Instant::now(),
|
||||
last_pong_received: Instant::now(),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the inner stream underlying the `IrcTransport`.
|
||||
pub fn into_inner(self) -> Framed<T, IrcCodec> {
|
||||
self.inner
|
||||
/// 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();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
/// Determines whether or not the transport has hit the ping timeout.
|
||||
fn ping_timed_out(&self) -> bool {
|
||||
if self.last_pong_received < self.last_ping_sent {
|
||||
self.last_ping_sent.elapsed().as_secs() >= self.ping_timeout
|
||||
} else {
|
||||
false
|
||||
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 last_ping_data = format!("{}", Local::now().timestamp());
|
||||
let data = last_ping_data.clone();
|
||||
let result = self.start_send(Command::PING(data, None).into())?;
|
||||
if let AsyncSink::Ready = result {
|
||||
self.poll_complete()?;
|
||||
// If we succeeded in sending the ping, we will update when the last ping was sent, and
|
||||
// the data that was sent with it.
|
||||
self.last_ping_sent = Instant::now();
|
||||
self.last_ping_data = last_ping_data;
|
||||
}
|
||||
let data = format!("{}", Local::now().timestamp());
|
||||
|
||||
self.tx
|
||||
.unbounded_send(Command::PING(data.clone(), None).into())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Polls the most recent burst window from the queue, always returning `NotReady` if none are
|
||||
/// left for whatever reason.
|
||||
fn rolling_burst_window_front(&mut self) -> Result<Async<()>, tokio_timer::TimerError> {
|
||||
self.rolling_burst_window.front_mut().map(|w| w.poll()).unwrap_or(Ok(Async::NotReady))
|
||||
/// 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<T> Stream for IrcTransport<T>
|
||||
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: AsyncRead + AsyncWrite,
|
||||
T: Unpin + AsyncRead + AsyncWrite,
|
||||
{
|
||||
type Item = Message;
|
||||
type Error = error::IrcError;
|
||||
/// 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));
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||
// If the ping timeout has been reached, we close the connection out and return an error.
|
||||
if self.ping_timed_out() {
|
||||
self.close()?;
|
||||
return Err(error::IrcError::PingTimeout)
|
||||
Transport { inner, pinger }
|
||||
}
|
||||
|
||||
// We poll both streams before doing any work because it is important to ensure that the
|
||||
// task is correctly woken up when they are ready.
|
||||
let timer_poll = self.ping_timer.poll()?;
|
||||
let inner_poll = self.inner.poll()?;
|
||||
|
||||
match (inner_poll, timer_poll) {
|
||||
// If neither the stream nor the ping timer are ready, the transport is not ready.
|
||||
(Async::NotReady, Async::NotReady) => Ok(Async::NotReady),
|
||||
|
||||
// If there's nothing available yet from the stream, but the ping timer is ready, we
|
||||
// simply send a ping and indicate that the transport has nothing to yield yet.
|
||||
(Async::NotReady, Async::Ready(msg)) => {
|
||||
assert!(msg.is_some());
|
||||
self.send_ping()?;
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
|
||||
// If the stream yields `None`, the connection has been terminated. Thus, we don't need
|
||||
// to worry about checking the ping timer, and can instead indicate that the transport
|
||||
// has been terminated.
|
||||
(Async::Ready(None), _) => Ok(Async::Ready(None)),
|
||||
|
||||
// If we have a new message available from the stream, we'll need to do some work, and
|
||||
// then yield the message.
|
||||
(Async::Ready(Some(msg)), _) => {
|
||||
// If the ping timer has returned, it is time to send another `PING` message!
|
||||
if let Async::Ready(msg) = timer_poll {
|
||||
assert!(msg.is_some());
|
||||
self.send_ping()?;
|
||||
}
|
||||
|
||||
match msg.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, _) => {
|
||||
let result = self.start_send(Command::PONG(data.to_owned(), None).into())?;
|
||||
assert!(result.is_ready());
|
||||
self.poll_complete()?;
|
||||
}
|
||||
|
||||
// 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(ref data, None) |
|
||||
Command::PONG(_, Some(ref data)) => {
|
||||
if self.last_ping_data == data[..] {
|
||||
self.last_pong_received = Instant::now();
|
||||
/// Gets the inner stream underlying the `Transport`.
|
||||
pub fn into_inner(self) -> Framed<T, IrcCodec> {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(Async::Ready(Some(msg)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sink for IrcTransport<T>
|
||||
impl<T> Stream for Transport<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
T: Unpin + AsyncRead + AsyncWrite,
|
||||
{
|
||||
type SinkItem = Message;
|
||||
type SinkError = error::IrcError;
|
||||
type Item = Result<Message, error::Error>;
|
||||
|
||||
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
|
||||
// If the ping timeout has been reached, we close the connection out and return an error.
|
||||
if self.ping_timed_out() {
|
||||
self.close()?;
|
||||
return Err(error::IrcError::PingTimeout)
|
||||
}
|
||||
|
||||
// Check if the oldest message in the rolling window is discounted.
|
||||
if let Async::Ready(()) = self.rolling_burst_window_front()? {
|
||||
self.current_burst_messages -= 1;
|
||||
self.rolling_burst_window.pop_front();
|
||||
}
|
||||
|
||||
// Throttling if too many messages have been sent recently.
|
||||
if self.current_burst_messages >= self.max_burst_messages {
|
||||
// When throttled, we know we need to finish sending what's already queued up.
|
||||
self.poll_complete()?;
|
||||
return Ok(AsyncSink::NotReady(item))
|
||||
}
|
||||
|
||||
match self.inner.start_send(item)? {
|
||||
AsyncSink::NotReady(item) => Ok(AsyncSink::NotReady(item)),
|
||||
AsyncSink::Ready => {
|
||||
self.current_burst_messages += 1;
|
||||
self.rolling_burst_window.push_back(self.burst_timer.sleep(Duration::from_secs(
|
||||
self.burst_window_length
|
||||
)));
|
||||
Ok(AsyncSink::Ready)
|
||||
}
|
||||
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 => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
|
||||
// If the ping timeout has been reached, we close the connection out and return an error.
|
||||
if self.ping_timed_out() {
|
||||
self.close()?;
|
||||
return Err(error::IrcError::PingTimeout)
|
||||
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)?;
|
||||
}
|
||||
|
||||
// If it's time to send a ping, we should do it! This is necessary to ensure that the
|
||||
// sink half will close even if the stream half closed without a ping timeout.
|
||||
if let Async::Ready(msg) = self.ping_timer.poll()? {
|
||||
assert!(msg.is_some());
|
||||
self.send_ping()?;
|
||||
Poll::Ready(Some(Ok(message)))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.inner.poll_complete()?)
|
||||
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 close(&mut self) -> Poll<(), Self::SinkError> {
|
||||
self.inner.close().map_err(|e| e.into())
|
||||
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(()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,25 +216,30 @@ pub struct LogView {
|
|||
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::IrcError::PoisonedLog)
|
||||
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::IrcError::PoisonedLog)
|
||||
self.received.read().map_err(|_| error::Error::PoisonedLog)
|
||||
}
|
||||
}
|
||||
|
||||
/// A logged version of the `IrcTransport` that records all sent and received messages.
|
||||
/// 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> where T: AsyncRead + AsyncWrite {
|
||||
inner: IrcTransport<T>,
|
||||
pub struct Logged<T> {
|
||||
inner: Transport<T>,
|
||||
view: LogView,
|
||||
}
|
||||
|
||||
impl<T> Logged<T> where T: AsyncRead + AsyncWrite {
|
||||
/// Wraps the given `IrcTransport` in logging.
|
||||
pub fn wrap(inner: IrcTransport<T>) -> Logged<T> {
|
||||
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 {
|
||||
|
@ -285,42 +257,55 @@ impl<T> Logged<T> where T: AsyncRead + AsyncWrite {
|
|||
|
||||
impl<T> Stream for Logged<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
T: Unpin + AsyncRead + AsyncWrite,
|
||||
{
|
||||
type Item = Message;
|
||||
type Error = error::IrcError;
|
||||
type Item = Result<Message, error::Error>;
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||
match try_ready!(self.inner.poll()) {
|
||||
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 recv: error::Result<_> = self.view.received.write().map_err(|_| {
|
||||
error::IrcError::PoisonedLog
|
||||
});
|
||||
recv?.push(msg.clone());
|
||||
Ok(Async::Ready(Some(msg)))
|
||||
let msg = msg?;
|
||||
|
||||
self.view
|
||||
.received
|
||||
.write()
|
||||
.map_err(|_| error::Error::PoisonedLog)?
|
||||
.push(msg.clone());
|
||||
|
||||
Poll::Ready(Some(Ok(msg)))
|
||||
}
|
||||
None => Ok(Async::Ready(None)),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Sink for Logged<T>
|
||||
impl<T> Sink<Message> for Logged<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite,
|
||||
T: Unpin + AsyncRead + AsyncWrite,
|
||||
{
|
||||
type SinkItem = Message;
|
||||
type SinkError = error::IrcError;
|
||||
type Error = error::Error;
|
||||
|
||||
fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
|
||||
let res = self.inner.start_send(item.clone())?;
|
||||
let sent: error::Result<_> = self.view.sent.write().map_err(|_| {
|
||||
error::IrcError::PoisonedLog
|
||||
});
|
||||
sent?.push(item);
|
||||
Ok(res)
|
||||
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_complete(&mut self) -> Poll<(), Self::SinkError> {
|
||||
Ok(self.inner.poll_complete()?)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
85
src/error.rs
85
src/error.rs
|
@ -4,29 +4,28 @@ use std::io::Error as IoError;
|
|||
use std::sync::mpsc::RecvError;
|
||||
|
||||
use failure;
|
||||
use futures::sync::mpsc::SendError;
|
||||
use futures::sync::oneshot::Canceled;
|
||||
use futures_channel::{
|
||||
mpsc::{SendError, TrySendError},
|
||||
oneshot::Canceled,
|
||||
};
|
||||
use native_tls::Error as TlsError;
|
||||
#[cfg(feature = "json")]
|
||||
use serde_json::Error as JsonError;
|
||||
#[cfg(feature = "yaml")]
|
||||
use serde_yaml::Error as YamlError;
|
||||
use tokio::executor::SpawnError;
|
||||
use tokio_timer::TimerError;
|
||||
#[cfg(feature = "toml")]
|
||||
use toml::de::Error as TomlReadError;
|
||||
#[cfg(feature = "toml")]
|
||||
use toml::ser::Error as TomlWriteError;
|
||||
|
||||
use proto::Message;
|
||||
use proto::error::{ProtocolError, MessageParseError};
|
||||
use crate::proto::error::{MessageParseError, ProtocolError};
|
||||
|
||||
/// A specialized `Result` type for the `irc` crate.
|
||||
pub type Result<T> = ::std::result::Result<T, IrcError>;
|
||||
pub type Result<T> = ::std::result::Result<T, Error>;
|
||||
|
||||
/// The main crate-wide error type.
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum IrcError {
|
||||
pub enum Error {
|
||||
/// An internal I/O error.
|
||||
#[fail(display = "an io error occurred")]
|
||||
Io(#[cause] IoError),
|
||||
|
@ -35,26 +34,18 @@ pub enum IrcError {
|
|||
#[fail(display = "a TLS error occurred")]
|
||||
Tls(#[cause] TlsError),
|
||||
|
||||
/// An error caused by Tokio being unable to spawn a task.
|
||||
#[fail(display = "unable to spawn task")]
|
||||
Spawn(#[cause] SpawnError),
|
||||
|
||||
/// An internal synchronous channel closed.
|
||||
#[fail(display = "a sync channel closed")]
|
||||
SyncChannelClosed(#[cause] RecvError),
|
||||
|
||||
/// An internal asynchronous channel closed.
|
||||
#[fail(display = "an async channel closed")]
|
||||
AsyncChannelClosed(#[cause] SendError<Message>),
|
||||
AsyncChannelClosed(#[cause] SendError),
|
||||
|
||||
/// An internal oneshot channel closed.
|
||||
#[fail(display = "a oneshot channel closed")]
|
||||
OneShotCanceled(#[cause] Canceled),
|
||||
|
||||
/// An internal timer error.
|
||||
#[fail(display = "timer failed")]
|
||||
Timer(#[cause] TimerError),
|
||||
|
||||
/// Error for invalid configurations.
|
||||
#[fail(display = "invalid config: {}", path)]
|
||||
InvalidConfig {
|
||||
|
@ -103,13 +94,17 @@ pub enum IrcError {
|
|||
#[fail(display = "none of the specified nicknames were usable")]
|
||||
NoUsableNick,
|
||||
|
||||
/// Stream has already been configured.
|
||||
#[fail(display = "stream has already been configured")]
|
||||
StreamAlreadyConfigured,
|
||||
|
||||
/// This allows you to produce any `failure::Error` within closures used by
|
||||
/// the irc crate. No errors of this kind will ever be produced by the crate
|
||||
/// itself.
|
||||
#[fail(display = "{}", inner)]
|
||||
Custom {
|
||||
/// The actual error that occurred.
|
||||
inner: failure::Error
|
||||
inner: failure::Error,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -170,55 +165,49 @@ pub enum TomlError {
|
|||
Write(#[cause] TomlWriteError),
|
||||
}
|
||||
|
||||
impl From<ProtocolError> for IrcError {
|
||||
fn from(e: ProtocolError) -> IrcError {
|
||||
impl From<ProtocolError> for Error {
|
||||
fn from(e: ProtocolError) -> Error {
|
||||
match e {
|
||||
ProtocolError::Io(e) => IrcError::Io(e),
|
||||
ProtocolError::InvalidMessage { string, cause } => IrcError::InvalidMessage {
|
||||
string, cause
|
||||
},
|
||||
ProtocolError::Io(e) => Error::Io(e),
|
||||
ProtocolError::InvalidMessage { string, cause } => {
|
||||
Error::InvalidMessage { string, cause }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IoError> for IrcError {
|
||||
fn from(e: IoError) -> IrcError {
|
||||
IrcError::Io(e)
|
||||
impl From<IoError> for Error {
|
||||
fn from(e: IoError) -> Error {
|
||||
Error::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TlsError> for IrcError {
|
||||
fn from(e: TlsError) -> IrcError {
|
||||
IrcError::Tls(e)
|
||||
impl From<TlsError> for Error {
|
||||
fn from(e: TlsError) -> Error {
|
||||
Error::Tls(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpawnError> for IrcError {
|
||||
fn from(e: SpawnError) -> IrcError {
|
||||
IrcError::Spawn(e)
|
||||
impl From<RecvError> for Error {
|
||||
fn from(e: RecvError) -> Error {
|
||||
Error::SyncChannelClosed(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecvError> for IrcError {
|
||||
fn from(e: RecvError) -> IrcError {
|
||||
IrcError::SyncChannelClosed(e)
|
||||
impl From<SendError> for Error {
|
||||
fn from(e: SendError) -> Error {
|
||||
Error::AsyncChannelClosed(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SendError<Message>> for IrcError {
|
||||
fn from(e: SendError<Message>) -> IrcError {
|
||||
IrcError::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 IrcError {
|
||||
fn from(e: Canceled) -> IrcError {
|
||||
IrcError::OneShotCanceled(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimerError> for IrcError {
|
||||
fn from(e: TimerError) -> IrcError {
|
||||
IrcError::Timer(e)
|
||||
impl From<Canceled> for Error {
|
||||
fn from(e: Canceled) -> Error {
|
||||
Error::OneShotCanceled(e)
|
||||
}
|
||||
}
|
||||
|
|
46
src/lib.rs
46
src/lib.rs
|
@ -17,56 +17,36 @@
|
|||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # extern crate irc;
|
||||
//! use irc::client::prelude::*;
|
||||
//! use futures::prelude::*;
|
||||
//!
|
||||
//! # fn main() {
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() -> irc::error::Result<()> {
|
||||
//! // configuration is loaded from config.toml into a Config
|
||||
//! let client = IrcClient::new("config.toml").unwrap();
|
||||
//! let mut client = Client::new("config.toml").await?;
|
||||
//! // identify comes from ClientExt
|
||||
//! client.identify().unwrap();
|
||||
//! // for_each_incoming comes from Client
|
||||
//! client.for_each_incoming(|irc_msg| {
|
||||
//! // irc_msg is a Message
|
||||
//! if let Command::PRIVMSG(channel, message) = irc_msg.command {
|
||||
//! 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();
|
||||
//! }
|
||||
//! }
|
||||
//! }).unwrap();
|
||||
//! }
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
extern crate bufstream;
|
||||
extern crate bytes;
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
extern crate encoding;
|
||||
#[macro_use]
|
||||
extern crate futures;
|
||||
|
||||
pub extern crate irc_proto as proto;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate native_tls;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[cfg(feature = "json")]
|
||||
extern crate serde_json;
|
||||
#[cfg(feature = "yaml")]
|
||||
extern crate serde_yaml;
|
||||
extern crate tokio;
|
||||
extern crate tokio_codec;
|
||||
extern crate tokio_io;
|
||||
extern crate tokio_mockstream;
|
||||
extern crate tokio_timer;
|
||||
extern crate tokio_tls;
|
||||
#[cfg(feature = "toml")]
|
||||
extern crate toml;
|
||||
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
|
|
Loading…
Reference in a new issue