From 1f2a67f2d3868fc3bdf5822e11d79df56f532e24 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 14 Feb 2018 15:03:12 +0100 Subject: [PATCH 01/50] Edited the crate description and keywords a bit. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 52948c0..eec605a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "irc" version = "0.13.3" -description = "A simple, thread-safe, and async-friendly library for IRC clients." +description = "the irc crate – usable, async IRC for Rust " authors = ["Aaron Weiss "] license = "MPL-2.0" -keywords = ["irc", "client", "thread-safe", "async", "tokio"] +keywords = ["irc", "client", "thread-safe", "async", "tokio", "protocol"] categories = ["asynchronous", "network-programming"] documentation = "https://docs.rs/irc/" repository = "https://github.com/aatxe/irc" From c91d14a9495773fa2735b76e3c0bd58ee53d58de Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 18 Feb 2018 21:13:01 +0100 Subject: [PATCH 02/50] Fixed a small bug when using nochanlists. --- src/client/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 9376d13..7d2b1f5 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -462,7 +462,7 @@ impl ClientState { } #[cfg(feature = "nochanlists")] - fn handle_part(&self, src: &str, chan: &str) {} + fn handle_part(&self, _: &str, _: &str) {} #[cfg(not(feature = "nochanlists"))] fn handle_part(&self, src: &str, chan: &str) { @@ -513,7 +513,7 @@ impl ClientState { } #[cfg(feature = "nochanlists")] - fn handle_mode(&self, _: &str, _: &[Mode]) {} + fn handle_mode(&self, _: &str, _: &[Mode]) {} #[cfg(not(feature = "nochanlists"))] fn handle_mode(&self, chan: &str, modes: &[Mode]) { From 3847bcb5a66939593c29e8cec55a2a83776c4c26 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 18 Feb 2018 21:39:06 +0100 Subject: [PATCH 03/50] Always skip serializing config's path. --- src/client/data/config.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/data/config.rs b/src/client/data/config.rs index fe2767d..5e90878 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -93,6 +93,7 @@ pub struct Config { /// The path that this configuration was loaded from. /// /// This should not be specified in any configuration. It will automatically be handled by the library. + #[serde(skip_serializing)] pub path: Option, } From 363629f400569d00b330cb5e5570b383b371978c Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 18 Feb 2018 22:54:24 +0100 Subject: [PATCH 04/50] Changed ClientExt to be more generic. --- src/client/ext.rs | 141 ++++++++++++++++++++++++++++++---------------- src/client/mod.rs | 2 +- 2 files changed, 93 insertions(+), 50 deletions(-) diff --git a/src/client/ext.rs b/src/client/ext.rs index 316e433..0dd4568 100644 --- a/src/client/ext.rs +++ b/src/client/ext.rs @@ -36,7 +36,7 @@ //! server.identify().unwrap(); //! # } //! ``` -use std::borrow::ToOwned; +use std::string::ToString; #[cfg(feature = "ctcp")] use chrono::prelude::*; @@ -105,11 +105,11 @@ pub trait ClientExt: Client { } /// Sends a SASL AUTHENTICATE message with the specified data. - fn send_sasl(&self, data: &str) -> Result<()> + fn send_sasl(&self, data: S) -> Result<()> where Self: Sized, { - self.send(AUTHENTICATE(data.to_owned())) + self.send(AUTHENTICATE(data.to_string())) } /// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism. @@ -138,189 +138,227 @@ pub trait ClientExt: Client { } /// Sends a PONG with the specified message. - fn send_pong(&self, msg: &str) -> Result<()> + fn send_pong(&self, msg: S) -> Result<()> where Self: Sized, + S: ToString, { - self.send(PONG(msg.to_owned(), None)) + self.send(PONG(msg.to_string(), None)) } /// Joins the specified channel or chanlist. - fn send_join(&self, chanlist: &str) -> Result<()> + fn send_join(&self, chanlist: S) -> Result<()> where Self: Sized, + S: ToString, { - self.send(JOIN(chanlist.to_owned(), None, None)) + self.send(JOIN(chanlist.to_string(), None, None)) } /// Joins the specified channel or chanlist using the specified key or keylist. - fn send_join_with_keys(&self, chanlist: &str, keylist: &str) -> Result<()> + fn send_join_with_keys(&self, chanlist: &str, keylist: &str) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { - self.send(JOIN(chanlist.to_owned(), Some(keylist.to_owned()), None)) + self.send(JOIN(chanlist.to_string(), Some(keylist.to_string()), None)) } /// Parts the specified channel or chanlist. - fn send_part(&self, chanlist: &str) -> Result<()> + fn send_part(&self, chanlist: S) -> Result<()> where Self: Sized, + S: ToString, { - self.send(PART(chanlist.to_owned(), None)) + self.send(PART(chanlist.to_string(), None)) } /// Attempts to oper up using the specified username and password. - fn send_oper(&self, username: &str, password: &str) -> Result<()> + fn send_oper(&self, username: S1, password: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { - self.send(OPER(username.to_owned(), password.to_owned())) + self.send(OPER(username.to_string(), password.to_string())) } /// Sends a message to the specified target. - fn send_privmsg(&self, target: &str, message: &str) -> Result<()> + fn send_privmsg(&self, target: S1, message: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { + let message = message.to_string(); for line in message.split("\r\n") { - self.send(PRIVMSG(target.to_owned(), line.to_owned()))? + self.send(PRIVMSG(target.to_string(), line.to_string()))? } Ok(()) } /// Sends a notice to the specified target. - fn send_notice(&self, target: &str, message: &str) -> Result<()> + fn send_notice(&self, target: S1, message: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { + let message = message.to_string(); for line in message.split("\r\n") { - self.send(NOTICE(target.to_owned(), line.to_owned()))? + self.send(NOTICE(target.to_string(), line.to_string()))? } Ok(()) } /// Sets the topic of a channel or requests the current one. /// If `topic` is an empty string, it won't be included in the message. - fn send_topic(&self, channel: &str, topic: &str) -> Result<()> + fn send_topic(&self, channel: S1, topic: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { + let topic = topic.to_string(); self.send(TOPIC( - channel.to_owned(), + channel.to_string(), if topic.is_empty() { None } else { - Some(topic.to_owned()) + Some(topic) }, )) } /// Kills the target with the provided message. - fn send_kill(&self, target: &str, message: &str) -> Result<()> + fn send_kill(&self, target: S1, message: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { - self.send(KILL(target.to_owned(), message.to_owned())) + self.send(KILL(target.to_string(), message.to_string())) } /// Kicks the listed nicknames from the listed channels with a comment. /// If `message` is an empty string, it won't be included in the message. - fn send_kick(&self, chanlist: &str, nicklist: &str, message: &str) -> Result<()> + fn send_kick(&self, chanlist: S1, nicklist: S2, message: S3) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, + S3: ToString, { + let message = message.to_string(); self.send(KICK( - chanlist.to_owned(), - nicklist.to_owned(), + chanlist.to_string(), + nicklist.to_string(), if message.is_empty() { None } else { - Some(message.to_owned()) + Some(message) }, )) } /// Changes the modes for the specified target. - fn send_mode(&self, target: &str, modes: &[Mode]) -> Result<()> + fn send_mode(&self, target: S, modes: &[Mode]) -> Result<()> where Self: Sized, + S: ToString, T: ModeType, { - self.send(T::mode(target, modes)) + self.send(T::mode(&target.to_string(), modes)) } /// Changes the mode of the target by force. /// If `modeparams` is an empty string, it won't be included in the message. - fn send_samode(&self, target: &str, mode: &str, modeparams: &str) -> Result<()> + fn send_samode(&self, target: S1, mode: S2, modeparams: S3) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, + S3: ToString, { + let modeparams = modeparams.to_string(); self.send(SAMODE( - target.to_owned(), - mode.to_owned(), + target.to_string(), + mode.to_string(), if modeparams.is_empty() { None } else { - Some(modeparams.to_owned()) + Some(modeparams) }, )) } /// Forces a user to change from the old nickname to the new nickname. - fn send_sanick(&self, old_nick: &str, new_nick: &str) -> Result<()> + fn send_sanick(&self, old_nick: S1, new_nick: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { - self.send(SANICK(old_nick.to_owned(), new_nick.to_owned())) + self.send(SANICK(old_nick.to_string(), new_nick.to_string())) } /// Invites a user to the specified channel. - fn send_invite(&self, nick: &str, chan: &str) -> Result<()> + fn send_invite(&self, nick: S1, chan: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { - self.send(INVITE(nick.to_owned(), chan.to_owned())) + self.send(INVITE(nick.to_string(), chan.to_string())) } /// Quits the server entirely with a message. /// This defaults to `Powered by Rust.` if none is specified. - fn send_quit(&self, msg: &str) -> Result<()> + fn send_quit(&self, msg: S) -> Result<()> where Self: Sized, + S: ToString, { + let msg = msg.to_string(); self.send(QUIT(Some(if msg.is_empty() { - "Powered by Rust.".to_owned() + "Powered by Rust.".to_string() } else { - msg.to_owned() + msg }))) } /// Sends a CTCP-escaped message to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] - fn send_ctcp(&self, target: &str, msg: &str) -> Result<()> + fn send_ctcp(&self, target: S1, msg: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { - self.send_privmsg(target, &format!("\u{001}{}\u{001}", msg)[..]) + self.send_privmsg(target, &format!("\u{001}{}\u{001}", msg.to_string())[..]) } /// Sends an action command to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] - fn send_action(&self, target: &str, msg: &str) -> Result<()> + fn send_action(&self, target: S1, msg: S2) -> Result<()> where Self: Sized, + S1: ToString, + S2: ToString, { - self.send_ctcp(target, &format!("ACTION {}", msg)[..]) + self.send_ctcp(target, &format!("ACTION {}", msg.to_string())[..]) } /// Sends a finger request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] - fn send_finger(&self, target: &str) -> Result<()> + fn send_finger(&self, target: S) -> Result<()> where Self: Sized, + S: ToString, { self.send_ctcp(target, "FINGER") } @@ -328,9 +366,10 @@ pub trait ClientExt: Client { /// Sends a version request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] - fn send_version(&self, target: &str) -> Result<()> + fn send_version(&self, target: S) -> Result<()> where Self: Sized, + S: ToString, { self.send_ctcp(target, "VERSION") } @@ -338,9 +377,10 @@ pub trait ClientExt: Client { /// Sends a source request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] - fn send_source(&self, target: &str) -> Result<()> + fn send_source(&self, target: S) -> Result<()> where Self: Sized, + S: ToString, { self.send_ctcp(target, "SOURCE") } @@ -348,9 +388,10 @@ pub trait ClientExt: Client { /// Sends a user info request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] - fn send_user_info(&self, target: &str) -> Result<()> + fn send_user_info(&self, target: S) -> Result<()> where Self: Sized, + S: ToString, { self.send_ctcp(target, "USERINFO") } @@ -358,9 +399,10 @@ pub trait ClientExt: Client { /// Sends a finger request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] - fn send_ctcp_ping(&self, target: &str) -> Result<()> + fn send_ctcp_ping(&self, target: S) -> Result<()> where Self: Sized, + S: ToString, { let time = Local::now(); self.send_ctcp(target, &format!("PING {}", time.timestamp())[..]) @@ -369,9 +411,10 @@ pub trait ClientExt: Client { /// Sends a time request to the specified target. /// This requires the CTCP feature to be enabled. #[cfg(feature = "ctcp")] - fn send_time(&self, target: &str) -> Result<()> + fn send_time(&self, target: S) -> Result<()> where Self: Sized, + S: ToString, { self.send_ctcp(target, "TIME") } diff --git a/src/client/mod.rs b/src/client/mod.rs index 7d2b1f5..8e59444 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -381,7 +381,7 @@ impl ClientState { let config_chans = self.config().channels(); for chan in &config_chans { match self.config().channel_key(chan) { - Some(key) => self.send_join_with_keys(chan, key)?, + Some(key) => self.send_join_with_keys::<&str, &str>(chan, key)?, None => self.send_join(chan)?, } } From 7d744529e64b95dd5972bb338882453e38f54b65 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 21 Feb 2018 12:16:35 +0100 Subject: [PATCH 05/50] Eliminated possible panics in Config::get_option. --- src/client/data/config.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 5e90878..beabc00 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -469,13 +469,10 @@ impl Config { } /// Looks up the specified string in the options map. - /// This uses indexing, and thus panics when the string is not present. - /// This will also panic if used and there are no options. - pub fn get_option(&self, option: &str) -> &str { - self.options - .as_ref() - .map(|o| &o[&option.to_owned()][..]) - .unwrap() + pub fn get_option(&self, option: &str) -> Option<&str> { + self.options.as_ref().and_then(|o| { + o.get(&option.to_owned()).map(|s| &s[..]) + }) } /// Gets whether or not to use a mock connection for testing. From 4509336d7452633987ada5722412f5401757545f Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 21 Feb 2018 12:23:54 +0100 Subject: [PATCH 06/50] Fixed config tests after Config::get_option API change. --- src/client/data/config.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/data/config.rs b/src/client/data/config.rs index beabc00..e154a27 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -571,6 +571,7 @@ mod test { }, ..Default::default() }; - assert_eq!(cfg.get_option("testing"), "test"); + assert_eq!(cfg.get_option("testing"), Some("test")); + assert_eq!(cfg.get_option("not"), None); } } From c78770f7639f971ce29c9ed70e0826afb010cae8 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 21 Feb 2018 14:30:27 +0100 Subject: [PATCH 07/50] Added a custom error that wraps failure::Error to add error possibilities for API consumers. --- src/error.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 21d49dc..8177251 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,7 @@ use std::io::Error as IoError; use std::sync::mpsc::RecvError; +use failure; use futures::sync::mpsc::SendError; use futures::sync::oneshot::Canceled; use native_tls::Error as TlsError; @@ -94,7 +95,16 @@ pub enum IrcError { /// All specified nicknames were in use or unusable. #[fail(display = "none of the specified nicknames were usable")] - NoUsableNick + NoUsableNick, + + /// This allows you to produce any `failure::Error` within closures used by + /// the irc crate. No errors of this kind will ever be produced by the crate + /// itself. + #[fail(display = "{}", inner)] + Custom { + /// The actual error that occurred. + inner: failure::Error + }, } /// Errors that occur when parsing messages. From 4921127372974b20815350f4863c0bd1d5ea3b54 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Fri, 23 Feb 2018 20:54:05 +0100 Subject: [PATCH 08/50] Changed ping logic to eliminate a rare panic under heavy load (fixes #120). --- src/client/transport.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/client/transport.rs b/src/client/transport.rs index 938b3de..39dc2e5 100644 --- a/src/client/transport.rs +++ b/src/client/transport.rs @@ -69,12 +69,14 @@ where } fn send_ping(&mut self) -> error::Result<()> { - self.last_ping_sent = Instant::now(); - self.last_ping_data = format!("{}", Local::now().timestamp()); - let data = self.last_ping_data.clone(); + let last_ping_data = format!("{}", Local::now().timestamp()); + let data = last_ping_data.clone(); let result = self.start_send(Command::PING(data, None).into())?; - assert!(result.is_ready()); - self.poll_complete()?; + if let AsyncSink::Ready = result { + self.poll_complete()?; + self.last_ping_sent = Instant::now(); + self.last_ping_data = last_ping_data; + } Ok(()) } From 5612c50f5a7656be451fed99f1f90a88fdc417fa Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Mon, 26 Feb 2018 19:46:30 +0100 Subject: [PATCH 09/50] Added code coverage with cargo-travis. --- .travis.yml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index be06bf3..69066ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,28 @@ language: rust rust: stable -sudo: false +sudo: true + +cache: + - apt + - cargo + +addons: + apt: + packages: + - libcurl4-openssl-dev + - libelf-dev + - libdw-dev + - binutils-dev + - cmake + sources: + - kalakris-cmake + +before_script: + - export PATH=$HOME/.cargo/bin:$PATH + - cargo install cargo-update || echo "cargo-update already installed" + - cargo install cargo-travis || echo "cargo-travis already installed" + - cargo install-update -a # update outdated cached binaries + script: - chmod +x mktestconfig.sh - ./mktestconfig.sh @@ -9,6 +31,10 @@ script: - cargo test --verbose --features "toml yaml" - cargo test --doc - cargo test --verbose --no-default-features + +after_success: + - cargo coveralls + notifications: email: false irc: From cad03f81e6535ac7b5ae4ac7bcaa2ac837d819e7 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Mon, 26 Feb 2018 20:09:37 +0100 Subject: [PATCH 10/50] Revert "Added code coverage with cargo-travis." It seems to hit an internal compiler error, so, that's not a great plan. This reverts commit 5612c50f5a7656be451fed99f1f90a88fdc417fa. --- .travis.yml | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index 69066ad..be06bf3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,6 @@ language: rust rust: stable -sudo: true - -cache: - - apt - - cargo - -addons: - apt: - packages: - - libcurl4-openssl-dev - - libelf-dev - - libdw-dev - - binutils-dev - - cmake - sources: - - kalakris-cmake - -before_script: - - export PATH=$HOME/.cargo/bin:$PATH - - cargo install cargo-update || echo "cargo-update already installed" - - cargo install cargo-travis || echo "cargo-travis already installed" - - cargo install-update -a # update outdated cached binaries - +sudo: false script: - chmod +x mktestconfig.sh - ./mktestconfig.sh @@ -31,10 +9,6 @@ script: - cargo test --verbose --features "toml yaml" - cargo test --doc - cargo test --verbose --no-default-features - -after_success: - - cargo coveralls - notifications: email: false irc: From 7af436a7d26d4fe769bf2a1eaa00df6b1179b37f Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 7 Mar 2018 19:43:56 +0100 Subject: [PATCH 11/50] Added a downloads count badge to README because why not? --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 53596b4..612a395 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# the irc crate [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] [![Docs][doc-badge]][doc] [![Built with Spacemacs][bws]][sm] +# the irc crate [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] ![Downloads][dl-badge] [![Docs][doc-badge]][doc] [![Built with Spacemacs][bws]][sm] [ci-badge]: https://travis-ci.org/aatxe/irc.svg?branch=stable [ci]: https://travis-ci.org/aatxe/irc [cr-badge]: https://img.shields.io/crates/v/irc.svg [cr]: https://crates.io/crates/irc +[dl-badge]: https://img.shields.io/crates/d/irc.svg [doc-badge]: https://docs.rs/irc/badge.svg [doc]: https://docs.rs/irc [bws]: https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg From 121e2cc84458f49c024732dc559fa39236fc7b5e Mon Sep 17 00:00:00 2001 From: isra17 Date: Tue, 13 Mar 2018 13:18:58 -0400 Subject: [PATCH 12/50] Fix wrong RPL numeric formatting --- src/proto/command.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/proto/command.rs b/src/proto/command.rs index 7c0b70a..f0ec551 100644 --- a/src/proto/command.rs +++ b/src/proto/command.rs @@ -421,18 +421,14 @@ impl<'a> From<&'a Command> for String { Command::CHGHOST(ref u, ref h) => stringify("CHGHOST", &[u, h], None), Command::Response(ref resp, ref a, Some(ref s)) => { - stringify( - &format!("{}", *resp as u16), - &a.iter().map(|s| &s[..]).collect::>(), - Some(s), - ) + stringify(&format!("{:03}", *resp as u16), + &a.iter().map(|s| &s[..]).collect::>(), + Some(s)) } Command::Response(ref resp, ref a, None) => { - stringify( - &format!("{}", *resp as u16), - &a.iter().map(|s| &s[..]).collect::>(), - None, - ) + stringify(&format!("{:03}", *resp as u16), + &a.iter().map(|s| &s[..]).collect::>(), + None) } Command::Raw(ref c, ref a, Some(ref s)) => { stringify(c, &a.iter().map(|s| &s[..]).collect::>(), Some(s)) @@ -1763,3 +1759,16 @@ impl FromStr for BatchSubCommand { } } } + +#[cfg(test)] +mod test { + use super::Response; + use super::Command; + + #[test] + fn format_response() { + assert!(String::from(&Command::Response(Response::RPL_WELCOME, + vec!["foo".into()], + None)) == "001 foo"); + } +} From 9f15041d01647528350f17b030605d1275fe211a Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Mon, 26 Mar 2018 04:01:56 +0200 Subject: [PATCH 13/50] Added playbot_ng to the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 612a395..2d5da3a 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,14 @@ projects: - [alectro][alectro] — a terminal IRC client - [spilo][spilo] — a minimalistic IRC bouncer - [irc-bot.rs][ircbot] — a library for writing IRC bots +- [playbot_ng][playbot_ng] — a Rust-evaluating IRC bot in Rust - [bunnybutt-rs][bunnybutt] — an IRC bot for the [Feed The Beast Wiki][ftb-wiki] [alectro]: https://github.com/aatxe/alectro [spilo]: https://github.com/aatxe/spilo [ircbot]: https://github.com/8573/irc-bot.rs [bunnybutt]: https://github.com/FTB-Gamepedia/bunnybutt-rs +[playbot_ng]: https://github.com/panicbit/playbot_ng [ftb-wiki]: https://ftb.gamepedia.com/FTB_Wiki Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pulls) to add it! From e514c97688d3f8f3573ea83b639509b5d3a2663e Mon Sep 17 00:00:00 2001 From: panicbit Date: Mon, 26 Mar 2018 08:53:38 +0200 Subject: [PATCH 14/50] Use FnMut for registering client handlers --- src/client/reactor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/reactor.rs b/src/client/reactor.rs index 110f3bf..34bf937 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -138,8 +138,8 @@ impl IrcReactor { /// # } /// ``` pub fn register_client_with_handler( - &mut self, client: IrcClient, handler: F - ) where F: Fn(&IrcClient, Message) -> U + 'static, + &mut self, client: IrcClient, mut handler: F + ) where F: FnMut(&IrcClient, Message) -> U + 'static, U: IntoFuture + 'static { self.handlers.push(Box::new(client.stream().for_each(move |message| { handler(&client, message) From 221997007f3c9ecb07bb8248d691a79f84805fad Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Fri, 23 Feb 2018 21:05:03 +0100 Subject: [PATCH 15/50] Bumped version to 0.13.4. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eec605a..6d5478d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "irc" -version = "0.13.3" +version = "0.13.4" description = "the irc crate – usable, async IRC for Rust " authors = ["Aaron Weiss "] license = "MPL-2.0" From 76802272227644f2f58c268bd3fcd1d6b1eff11e Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Fri, 30 Mar 2018 21:49:26 +0200 Subject: [PATCH 16/50] Sanitize messages later to stop the problematic round-trip parsing. --- src/client/mod.rs | 34 +++++++++------------------------- src/proto/irc.rs | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 8e59444..c503e70 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -243,12 +243,9 @@ impl<'a> Client for ClientState { } fn send>(&self, msg: M) -> error::Result<()> where Self: Sized { - let msg = &msg.into(); - self.handle_sent_message(msg)?; - Ok((&self.outgoing).unbounded_send( - ClientState::sanitize(&msg.to_string()) - .into(), - )?) + let msg = msg.into(); + self.handle_sent_message(&msg)?; + Ok(self.outgoing.unbounded_send(msg)?) } fn stream(&self) -> ClientStream { @@ -302,22 +299,6 @@ impl ClientState { } } - /// Sanitizes the input string by cutting up to (and including) the first occurence of a line - /// terminiating phrase (`\r\n`, `\r`, or `\n`). This is used in sending messages back to - /// prevent the injection of additional commands. - fn sanitize(data: &str) -> &str { - // n.b. ordering matters here to prefer "\r\n" over "\r" - if let Some((pos, len)) = ["\r\n", "\r", "\n"] - .iter() - .flat_map(|needle| data.find(needle).map(|pos| (pos, needle.len()))) - .min_by_key(|&(pos, _)| pos) - { - data.split_at(pos + len).0 - } else { - data - } - } - /// Gets the current nickname in use. fn current_nickname(&self) -> &str { let alt_nicks = self.config().alternate_nicknames(); @@ -865,7 +846,7 @@ mod test { use client::data::Config; #[cfg(not(feature = "nochanlists"))] use client::data::User; - use proto::{ChannelMode, Mode}; + use proto::{ChannelMode, IrcCodec, Mode}; use proto::command::Command::{PART, PRIVMSG}; pub fn test_config() -> Config { @@ -886,7 +867,10 @@ mod test { // We can't guarantee that everything will have been sent by the time of this call. thread::sleep(Duration::from_millis(100)); client.log_view().sent().unwrap().iter().fold(String::new(), |mut acc, msg| { - acc.push_str(&msg.to_string()); + // NOTE: we have to sanitize here because sanitization happens in IrcCodec after the + // messages are converted into strings, but our transport logger catches messages before + // they ever reach that point. + acc.push_str(&IrcCodec::sanitize(msg.to_string())); acc }) } @@ -1055,7 +1039,7 @@ mod test { let res = client.for_each_incoming(|message| { println!("{:?}", message); }); - + if let Err(IrcError::NoUsableNick) = res { () } else { diff --git a/src/proto/irc.rs b/src/proto/irc.rs index e0b2fca..e10ae27 100644 --- a/src/proto/irc.rs +++ b/src/proto/irc.rs @@ -16,6 +16,22 @@ impl IrcCodec { pub fn new(label: &str) -> error::Result { LineCodec::new(label).map(|codec| IrcCodec { inner: codec }) } + + /// Sanitizes the input string by cutting up to (and including) the first occurence of a line + /// terminiating phrase (`\r\n`, `\r`, or `\n`). This is used in sending messages back to + /// prevent the injection of additional commands. + pub(crate) fn sanitize(mut data: String) -> String { + // n.b. ordering matters here to prefer "\r\n" over "\r" + if let Some((pos, len)) = ["\r\n", "\r", "\n"] + .iter() + .flat_map(|needle| data.find(needle).map(|pos| (pos, needle.len()))) + .min_by_key(|&(pos, _)| pos) + { + data.truncate(pos + len); + } + data + } + } impl Decoder for IrcCodec { @@ -35,6 +51,6 @@ impl Encoder for IrcCodec { fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> { - self.inner.encode(msg.to_string(), dst) + self.inner.encode(IrcCodec::sanitize(msg.to_string()), dst) } } From cef0462859ab208b192572f737ab508b31916970 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Fri, 30 Mar 2018 21:56:29 +0200 Subject: [PATCH 17/50] Added a regression test for sending raw messages (see #128). --- src/client/mod.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index c503e70..f103d5b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -847,7 +847,7 @@ mod test { #[cfg(not(feature = "nochanlists"))] use client::data::User; use proto::{ChannelMode, IrcCodec, Mode}; - use proto::command::Command::{PART, PRIVMSG}; + use proto::command::Command::{PART, PRIVMSG, Raw}; pub fn test_config() -> Config { Config { @@ -1072,6 +1072,18 @@ mod test { assert_eq!(&get_client_value(client)[..], "PRIVMSG #test :Hi there!\r\n"); } + #[test] + fn send_raw_is_really_raw() { + let client = IrcClient::from_config(test_config()).unwrap(); + assert!( + client.send(Raw("PASS".to_owned(), vec!["password".to_owned()], None)).is_ok() + ); + assert!( + client.send(Raw("NICK".to_owned(), vec!["test".to_owned()], None)).is_ok() + ); + assert_eq!(&get_client_value(client)[..], "PASS password\r\nNICK test\r\n"); + } + #[test] #[cfg(not(feature = "nochanlists"))] fn channel_tracking_names() { From 845ca63416fe408da4b7784d1bc7cdc30cf17ee8 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Fri, 30 Mar 2018 22:37:24 +0200 Subject: [PATCH 18/50] Track users who are kicked out of channels (fixes #127). --- src/client/mod.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index f103d5b..0a05f56 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -67,7 +67,7 @@ use client::data::{Config, User}; use client::ext::ClientExt; use client::transport::LogView; use proto::{ChannelMode, Command, Message, Mode, Response}; -use proto::Command::{JOIN, NICK, NICKSERV, PART, PRIVMSG, ChannelMODE, QUIT}; +use proto::Command::{JOIN, KICK, NICK, NICKSERV, PART, PRIVMSG, ChannelMODE, QUIT}; pub mod conn; pub mod data; @@ -329,6 +329,7 @@ impl ClientState { match msg.command { JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan), PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan), + KICK(ref chan, ref user, _) => self.handle_part(user, chan), QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")), NICK(ref new_nick) => { self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick) @@ -1153,6 +1154,27 @@ mod test { ) } + #[test] + #[cfg(not(feature = "nochanlists"))] + fn user_tracking_names_kick() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ + :owner!test@test KICK #test test\r\n"; + let client = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + client.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + client.list_users("#test").unwrap(), + vec![ + User::new("&admin"), + User::new("~owner"), + ] + ) + } + #[test] #[cfg(not(feature = "nochanlists"))] fn user_tracking_names_part() { From cba63a80c64c1b263baa9e3d1d59546a66e8f7a0 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Fri, 30 Mar 2018 22:18:59 +0200 Subject: [PATCH 19/50] Bumped version to 0.13.5. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6d5478d..d263fe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "irc" -version = "0.13.4" +version = "0.13.5" description = "the irc crate – usable, async IRC for Rust " authors = ["Aaron Weiss "] license = "MPL-2.0" From 30288c51f88abf93f6f9e31b52392715c7c63afb Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 31 Mar 2018 16:03:19 +0200 Subject: [PATCH 20/50] Made README just a little bit better for docs links. --- README.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2d5da3a..b64055e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ compliant with [RFC 2812][rfc2812], [IRCv3.1][ircv3.1], [IRCv3.2][ircv3.2], and additional, common features from popular IRCds. You can find up-to-date, ready-to-use documentation online [on docs.rs][doc]. -## Built with the irc crate ## +## Built with the irc crate the irc crate is being used to build new IRC software in Rust. Here are some of our favorite projects: @@ -39,16 +39,18 @@ projects: Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pulls) to add it! -## Getting Started ## +## Getting Started To start using the irc crate with cargo, you can simply add `irc = "0.13"` to your dependencies in -your Cargo.toml file. The high-level API can be found `irc::client::prelude` linked to from the -[doc root][doc]. You'll find a number of examples in `examples/`, throughout the documentation, and -below. +your Cargo.toml file. The high-level API can be found in [`irc::client::prelude`][irc-prelude]. +You'll find a number of examples to help you get started in `examples/`, throughout the +documentation, and below. -## A Tale of Two APIs ## +[irc-prelude]: https://docs.rs/irc/*/irc/client/prelude/index.html -### Reactors (The "New" API) ### +## A Tale of Two APIs + +### Reactors (The "New" API) The release of v0.13 brought with it a new API called `IrcReactor` that enables easier multiserver support and more graceful error handling. The general model is that you use the reactor to create @@ -84,7 +86,7 @@ fn main() { ``` -### Direct Style (The "Old" API) ### +### Direct Style (The "Old" API) The old API for connecting to an IRC server is still supported through the `IrcClient` type. It's simpler for the most basic use case, but will panic upon encountering any sort of connection issues. @@ -116,7 +118,7 @@ fn main() { } ``` -## Configuring IRC Clients ## +## Configuring IRC Clients As seen above, there are two techniques for configuring the irc crate: runtime loading and programmatic configuration. Runtime loading is done via the function `Config::load`, and is likely @@ -125,8 +127,9 @@ also be useful when defining your own custom configuration format that can be co The primary configuration format is TOML, but if you are so inclined, you can use JSON and/or YAML via the optional `json` and `yaml` features respectively. At the minimum, a configuration requires `nickname` and `server` to be defined, and all other fields are optional. You can find detailed -explanations of the various fields on -[docs.rs](https://docs.rs/irc/0.13.2/irc/client/data/config/struct.Config.html#fields). +explanations of the various fields on [docs.rs][config-fields]. + +[config-fields]: https://docs.rs/irc/*/irc/client/data/config/struct.Config.html#fields Alternatively, you can look at the example below of a TOML configuration with all the fields: @@ -173,7 +176,7 @@ cargo run --example convertconf -- -i client_config.json -o client_config.toml Note that the formats are automatically determined based on the selected file extensions. This tool should make it easy for users to migrate their old configurations to TOML. -## Contributing ## +## Contributing the irc crate is a free, open source library that relies on contributions from its maintainers, Aaron Weiss ([@aatxe][awe]) and Peter Atashian ([@retep998][bun]), as well as the broader Rust community. It's licensed under the Mozilla Public License 2.0 whose text can be found in From f2e10001c03afe551cbdd174a8727a7ef143e960 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 31 Mar 2018 16:22:07 +0200 Subject: [PATCH 21/50] Improved the documentation for `Config`. --- src/client/data/config.rs | 47 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/client/data/config.rs b/src/client/data/config.rs index e154a27..1f338bc 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -18,7 +18,51 @@ use error::TomlError; use error::{ConfigError, Result}; use error::IrcError::InvalidConfig; -/// Configuration data. +/// Configuration for IRC clients. +/// +/// # Building a configuration programmatically +/// +/// For some use cases, it may be useful to build configurations programmatically. Since `Config` is +/// an ordinary struct with public fields, this should be rather straightforward. However, it is +/// important to note that the use of `Config::default()` is important, even when specifying all +/// visible fields because `Config` keeps track of whether it was loaded from a file or +/// programmatically defined, in order to produce better error messages. Using `Config::default()` +/// as below will ensure that this process is handled correctly. +/// +/// ``` +/// # extern crate irc; +/// use irc::client::prelude::Config; +/// +/// # fn main() { +/// let config = Config { +/// nickname: Some("test".to_owned()), +/// server: Some("irc.example.com".to_owned()), +/// ..Config::default() +/// }; +/// # } +/// ``` +/// +/// # Loading a configuration from a file +/// +/// The standard method of using a configuration is to load it from a TOML file. You can find an +/// example TOML configuration in the README, as well as a minimal example with code for loading the +/// configuration below. +/// +/// ## TOML (`config.toml`) +/// ```toml +/// nickname = "test" +/// server = "irc.example.com" +/// ``` +/// +/// ## Rust +/// ```no_run +/// # extern crate irc; +/// use irc::client::prelude::Config; +/// +/// # fn main() { +/// let config = Config::load("config.toml").unwrap(); +/// # } +/// ``` #[derive(Clone, Deserialize, Serialize, Default, PartialEq, Debug)] pub struct Config { /// A list of the owners of the client by nickname (for bots). @@ -94,6 +138,7 @@ pub struct Config { /// /// This should not be specified in any configuration. It will automatically be handled by the library. #[serde(skip_serializing)] + #[doc(hidden)] pub path: Option, } From e80a0f3a891e8ab160d723bf55689d64e7930b93 Mon Sep 17 00:00:00 2001 From: "Alex S. Glomsaas" Date: Sat, 31 Mar 2018 21:09:42 +0200 Subject: [PATCH 22/50] Implement CertFP resolves #131 --- README.md | 2 ++ src/client/conn.rs | 11 ++++++++++- src/client/data/config.rs | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b64055e..febb667 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,8 @@ port = 6697 password = "" use_ssl = true cert_path = "cert.der" +client_cert_path = "client.der" +client_cert_pass = "password" encoding = "UTF-8" channels = ["#rust", "#haskell", "#fake"] umodes = "+RB-x" diff --git a/src/client/conn.rs b/src/client/conn.rs index 0bb11bc..e9552d2 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -6,7 +6,7 @@ use std::io::Read; use encoding::EncoderTrap; use encoding::label::encoding_from_whatwg_label; use futures::{Async, Poll, Future, Sink, StartSend, Stream}; -use native_tls::{Certificate, TlsConnector}; +use native_tls::{Certificate, TlsConnector, Pkcs12}; use tokio_core::reactor::Handle; use tokio_core::net::{TcpStream, TcpStreamNew}; use tokio_io::AsyncRead; @@ -135,6 +135,15 @@ impl Connection { builder.add_root_certificate(cert)?; info!("Added {} to trusted certificates.", cert_path); } + if let Some(client_cert_path) = config.client_cert_path() { + let client_cert_pass = config.client_cert_pass(); + let mut file = File::open(client_cert_path)?; + let mut client_cert_data = vec![]; + file.read_to_end(&mut client_cert_data)?; + let pkcs12_archive = Pkcs12::from_der(&client_cert_data, &client_cert_pass)?; + builder.identity(pkcs12_archive)?; + info!("Using {} for client certificate authentication.", client_cert_path); + } let connector = builder.build()?; let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle).map_err(|e| { let res: error::IrcError = e.into(); diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 1f338bc..f72d99e 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -88,6 +88,10 @@ pub struct Config { pub use_ssl: Option, /// The path to the SSL certificate for this server in DER format. pub cert_path: Option, + /// The path to a SSL certificate to use for CertFP client authentication in DER format. + pub client_cert_path: Option, + /// The password for the certificate to use in CertFP authentication. + pub client_cert_pass: Option, /// The encoding type used for this connection. /// This is typically UTF-8, but could be something else. pub encoding: Option, @@ -419,6 +423,16 @@ impl Config { self.cert_path.as_ref().map(|s| &s[..]) } + /// Gets the path to the client authentication certificate in DER format if specified. + pub fn client_cert_path(&self) -> Option<&str> { + self.client_cert_path.as_ref().map(|s| &s[..]) + } + + /// Gets the password to the client authentication certificate. + pub fn client_cert_pass(&self) -> &str { + self.client_cert_pass.as_ref().map_or("", |s| &s[..]) + } + /// Gets the encoding to use for this connection. This requires the encode feature to work. /// This defaults to UTF-8 when not specified. pub fn encoding(&self) -> &str { @@ -557,6 +571,8 @@ mod test { port: Some(6667), use_ssl: Some(false), cert_path: None, + client_cert_path: None, + client_cert_pass: None, encoding: Some(format!("UTF-8")), channels: Some(vec![format!("#test"), format!("#test2")]), channel_keys: None, From 654b759c022541abce206d7aee82eb40f293e507 Mon Sep 17 00:00:00 2001 From: Frederik B Date: Sat, 31 Mar 2018 21:18:23 +0200 Subject: [PATCH 23/50] add support for formatted strings (fixes #130) --- src/proto/colors.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++ src/proto/mod.rs | 2 + 2 files changed, 96 insertions(+) create mode 100644 src/proto/colors.rs diff --git a/src/proto/colors.rs b/src/proto/colors.rs new file mode 100644 index 0000000..7da5e61 --- /dev/null +++ b/src/proto/colors.rs @@ -0,0 +1,94 @@ +//! An extension trait that provides the ability to strip IRC colors from a string +use std::borrow::Cow; + +#[derive(PartialEq)] +enum ParserState { + Text, + ColorCode, + Foreground1, + Foreground2, + Comma, + Background1, +} +struct Parser { + state: ParserState, +} + +/// An extension trait giving strings a function to strip IRC colors +pub trait FormattedStringExt { + + /// 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; + +} + + +impl FormattedStringExt for str { + fn is_formatted(&self) -> bool { + self.contains('\x02') || // bold + self.contains('\x1F') || // underline + self.contains('\x16') || // reverse + self.contains('\x0F') || // normal + self.contains('\x03') // color + } + + fn strip_formatting(&self) -> Cow { + let mut parser = Parser { + state: ParserState::Text, + }; + + let result: Cow = self + .chars() + .filter(move |cur| { + match parser.state { + ParserState::Text if *cur == '\x03' => { + parser.state = ParserState::ColorCode; + false + }, + ParserState::Text => !['\x02', '\x1F', '\x16', '\x0F'].contains(cur), + ParserState::ColorCode if (*cur).is_digit(10) => { + parser.state = ParserState::Foreground1; + false + }, + ParserState::Foreground1 if (*cur).is_digit(6) => { + parser.state = ParserState::Foreground2; + false + }, + ParserState::Foreground1 if *cur == ',' => { + parser.state = ParserState::Comma; + false + }, + ParserState::Foreground2 if *cur == ',' => { + parser.state = ParserState::Comma; + false + }, + ParserState::Comma if ((*cur).is_digit(10)) => { + parser.state = ParserState::Background1; + false + }, + ParserState::Background1 if (*cur).is_digit(6) => { + parser.state = ParserState::Text; + false + } + _ => true + } + }) + .collect(); + + result + } + + +} + +impl FormattedStringExt for String { + fn is_formatted(&self) -> bool { + (&self[..]).is_formatted() + } + fn strip_formatting(&self) -> Cow { + (&self[..]).strip_formatting() + } +} diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 6d46691..e27925d 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -3,6 +3,7 @@ pub mod caps; pub mod chan; pub mod command; +pub mod colors; pub mod irc; pub mod line; pub mod message; @@ -11,6 +12,7 @@ pub mod response; pub use self::caps::{Capability, NegotiationVersion}; pub use self::chan::ChannelExt; +pub use self::colors::FormattedStringExt; pub use self::command::{BatchSubCommand, CapSubCommand, Command}; pub use self::irc::IrcCodec; pub use self::message::Message; From 30dd75658879e52940191d6733113d2b4a69fa46 Mon Sep 17 00:00:00 2001 From: Frederik B Date: Sun, 15 Apr 2018 22:29:48 +0200 Subject: [PATCH 24/50] add tests for formatted strings (#130) --- src/proto/colors.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index 7da5e61..e448e46 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -92,3 +92,40 @@ impl FormattedStringExt for String { (&self[..]).strip_formatting() } } + +#[cfg(test)] +mod test { + use proto::colors::FormattedStringExt; + + #[test] + fn test_strip_bold() { + assert_eq!("l\x02ol".strip_formatting(), "lol"); + } + + #[test] + fn test_strip_fg_color() { + assert_eq!("l\x033ol".strip_formatting(), "lol"); + } + + #[test] + fn test_strip_fg_color2() { + assert_eq!("l\x0312ol".strip_formatting(), "lol"); + } + + #[test] + fn test_strip_fg_bg_11() { + assert_eq!("l\x031,2ol".strip_formatting(), "lol"); + } + #[test] + fn test_strip_fg_bg_21() { + assert_eq!("l\x0312,3ol".strip_formatting(), "lol"); + } + #[test] + fn test_strip_fg_bg_12() { + assert_eq!("l\x031,12ol".strip_formatting(), "lol"); + } + #[test] + fn test_strip_fg_bg_22() { + assert_eq!("l\x0312,13ol".strip_formatting(), "lol"); + } +} \ No newline at end of file From 5833403ffda57c7d5e5ea1be7a69891b62b387ec Mon Sep 17 00:00:00 2001 From: Frederik B Date: Sat, 21 Apr 2018 07:57:47 +0200 Subject: [PATCH 25/50] parse 2nd color digit only if within (0..16) --- src/proto/colors.rs | 48 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index e448e46..f86548c 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -39,12 +39,12 @@ impl FormattedStringExt for str { let mut parser = Parser { state: ParserState::Text, }; - + let mut prev: char = '\x00'; let result: Cow = self .chars() .filter(move |cur| { - match parser.state { - ParserState::Text if *cur == '\x03' => { + let result = match parser.state { + ParserState::Text | ParserState::Foreground1 | ParserState::Foreground2 if *cur == '\x03' => { parser.state = ParserState::ColorCode; false }, @@ -54,8 +54,14 @@ impl FormattedStringExt for str { false }, ParserState::Foreground1 if (*cur).is_digit(6) => { - parser.state = ParserState::Foreground2; - false + // can only consume another digit if previous char was 1. + if (prev) == '1' { + parser.state = ParserState::Foreground2; + false + } else { + parser.state = ParserState::Text; + true + } }, ParserState::Foreground1 if *cur == ',' => { parser.state = ParserState::Comma; @@ -70,11 +76,21 @@ impl FormattedStringExt for str { false }, ParserState::Background1 if (*cur).is_digit(6) => { + // can only consume another digit if previous char was 1. parser.state = ParserState::Text; - false + if (prev) == '1' { + false + } else { + true + } } - _ => true - } + _ => { + parser.state = ParserState::Text; + true + } + }; + prev = *cur; + return result }) .collect(); @@ -128,4 +144,20 @@ mod test { fn test_strip_fg_bg_22() { assert_eq!("l\x0312,13ol".strip_formatting(), "lol"); } + #[test] + fn test_strip_string_with_multiple_colors() { + assert_eq!("hoo\x034r\x033a\x0312y".strip_formatting(), "hooray"); + } + #[test] + fn test_strip_string_with_digit_after_color() { + assert_eq!("\x0344\x0355\x0366".strip_formatting(), "456"); + } + #[test] + fn test_strip_string_with_multiple_2digit_colors() { + assert_eq!("hoo\x0310r\x0311a\x0312y".strip_formatting(), "hooray"); + } + #[test] + fn test_strip_string_with_digit_after_2digit_color() { + assert_eq!("\x031212\x031111\x031010".strip_formatting(), "121110"); + } } \ No newline at end of file From 61e1f16c21d8f37bbdec775564f77b19c0093778 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 31 Mar 2018 22:29:27 +0200 Subject: [PATCH 26/50] Added the is-it-maintained badges to Cargo.toml. --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index d263fe8..61165ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ readme = "README.md" [badges] travis-ci = { repository = "aatxe/irc" } +is-it-maintained-issue-resolution = { repository = "aatxe/irc" } +is-it-maintained-open-issues = { repository = "aatxe/irc" } [features] default = ["ctcp", "toml"] From 88233f8e2d534d2dddbbe463ffdafa6c17da0c29 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 22 Apr 2018 01:20:58 +0200 Subject: [PATCH 27/50] Removed spacemacs shilling from README. I still love you, spacemacs, but the header is too long on crates.io --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index febb667..89a4b7b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# the irc crate [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] ![Downloads][dl-badge] [![Docs][doc-badge]][doc] [![Built with Spacemacs][bws]][sm] +# the irc crate [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] ![Downloads][dl-badge] [![Docs][doc-badge]][doc] [ci-badge]: https://travis-ci.org/aatxe/irc.svg?branch=stable [ci]: https://travis-ci.org/aatxe/irc @@ -7,8 +7,6 @@ [dl-badge]: https://img.shields.io/crates/d/irc.svg [doc-badge]: https://docs.rs/irc/badge.svg [doc]: https://docs.rs/irc -[bws]: https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg -[sm]: http://spacemacs.org [rfc2812]: http://tools.ietf.org/html/rfc2812 [ircv3.1]: http://ircv3.net/irc/3.1.html From 6569f079c7ed39afa044df9dba477b0e69d3c8d7 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 2 May 2018 14:20:37 +0200 Subject: [PATCH 28/50] Added a section to the contributing docs about mentoring. --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2eb45ee..09d425e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,14 @@ possible for everyone, there are a few guidelines that we need contributors to f * Make sure you have a [GitHub account](https://github.com/signup/free). * Fork the repository on GitHub. +## Finding Something to Do + +There will almost certainly be issues open on the irc crate at all times. These are a good place to +start if you don't have anything pressing on your mind that you'd like to see. If you need or would +like mentoring instructions on any issue, please ping @aatxe, and he will do his best to provide +them in a timely fashion. If instead you have your own idea and would like mentoring or feedback, +you should open a new issue and mention such a desire. + ## Making Changes * Create a topic branch from where you want to base your work. From a8a48bf4a14654e41af98e9c6598fb2808551688 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 2 May 2018 14:21:45 +0200 Subject: [PATCH 29/50] Changed reference to my account in contributing docs to an actual link. --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09d425e..4c0d3f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,9 +14,9 @@ possible for everyone, there are a few guidelines that we need contributors to f There will almost certainly be issues open on the irc crate at all times. These are a good place to start if you don't have anything pressing on your mind that you'd like to see. If you need or would -like mentoring instructions on any issue, please ping @aatxe, and he will do his best to provide -them in a timely fashion. If instead you have your own idea and would like mentoring or feedback, -you should open a new issue and mention such a desire. +like mentoring instructions on any issue, please ping [@aatxe](https://github.com/aatxe), and he +will do his best to provide them in a timely fashion. If instead you have your own idea and would +like mentoring or feedback, you should open a new issue and mention such a desire. ## Making Changes From acc6d7f0f0f5246ba6f2040ddc5b10eaf93511a3 Mon Sep 17 00:00:00 2001 From: Frederik B Date: Wed, 16 May 2018 11:15:43 +0200 Subject: [PATCH 30/50] Remove unnecessary [..] Replaces (&self[..]).strip_formatting() with (&self).strip_formatting() in `impl FormattedStringExt for String` --- src/proto/colors.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index f86548c..6ffb492 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -102,10 +102,10 @@ impl FormattedStringExt for str { impl FormattedStringExt for String { fn is_formatted(&self) -> bool { - (&self[..]).is_formatted() + (&self).is_formatted() } fn strip_formatting(&self) -> Cow { - (&self[..]).strip_formatting() + (&self).strip_formatting() } } @@ -160,4 +160,4 @@ mod test { fn test_strip_string_with_digit_after_2digit_color() { assert_eq!("\x031212\x031111\x031010".strip_formatting(), "121110"); } -} \ No newline at end of file +} From a23f3417f18bd150baa9634da7ae16a76a5f96ad Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 27 May 2018 23:15:05 +0200 Subject: [PATCH 31/50] Added the tooter example, an async version of tweeter. --- examples/tooter.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++ examples/tweeter.rs | 1 + 2 files changed, 52 insertions(+) create mode 100644 examples/tooter.rs diff --git a/examples/tooter.rs b/examples/tooter.rs new file mode 100644 index 0000000..aee8ab9 --- /dev/null +++ b/examples/tooter.rs @@ -0,0 +1,51 @@ +extern crate irc; +extern crate tokio_timer; + +use std::default::Default; +use std::time::Duration; +use irc::client::prelude::*; +use irc::error::IrcError; + +// NOTE: this example is a conversion of `tweeter.rs` to an asynchronous style with `IrcReactor`. +fn main() { + let config = Config { + nickname: Some("mastodon".to_owned()), + server: Some("irc.fyrechat.net".to_owned()), + channels: Some(vec!["#irc-crate".to_owned()]), + ..Default::default() + }; + + // We need to create a reactor first and foremost + let mut reactor = IrcReactor::new().unwrap(); + // and then create a client via its API. + let client = reactor.prepare_client_and_connect(&config).unwrap(); + // Then, we identify + client.identify().unwrap(); + // and clone just as before. + let send_client = client.clone(); + + // Rather than spawn a thread that reads the messages separately, we register a handler with the + // reactor. just as in the original version, we don't do any real handling and instead just print + // the messages that are received. + reactor.register_client_with_handler(client, |_, message| { + print!("{}", message); + Ok(()) + }); + + // We construct an interval using a wheel timer from tokio_timer. This interval will fire + // every 10 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("#irc-crate", "TOOT TOOT") + })); + + // Then, on the main thread, we finally run the reactor which blocks the program indefinitely. + reactor.run().unwrap(); +} diff --git a/examples/tweeter.rs b/examples/tweeter.rs index 94de21a..830ca0d 100644 --- a/examples/tweeter.rs +++ b/examples/tweeter.rs @@ -5,6 +5,7 @@ use std::thread; use std::time::Duration; use irc::client::prelude::*; +// NOTE: you can find an asynchronous version of this example with `IrcReactor` in `tooter.rs`. fn main() { let config = Config { nickname: Some("pickles".to_owned()), From d4283f6071815104326250441401004df84d86a4 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 27 May 2018 23:24:37 +0200 Subject: [PATCH 32/50] Rewrapped comments in tooter.rs. --- examples/tooter.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/tooter.rs b/examples/tooter.rs index aee8ab9..42a2ad8 100644 --- a/examples/tooter.rs +++ b/examples/tooter.rs @@ -25,15 +25,15 @@ fn main() { let send_client = client.clone(); // Rather than spawn a thread that reads the messages separately, we register a handler with the - // reactor. just as in the original version, we don't do any real handling and instead just print - // the messages that are received. + // reactor. just as in the original version, we don't do any real handling and instead just + // print the messages that are received. reactor.register_client_with_handler(client, |_, message| { print!("{}", message); Ok(()) }); - // We construct an interval using a wheel timer from tokio_timer. This interval will fire - // every 10 seconds (and is roughly accurate to the second). + // 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) From 791edb2bd724cb433efeaa6efc6b2b19e466b340 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Tue, 5 Jun 2018 20:04:06 +0200 Subject: [PATCH 33/50] Added a small optimization (suggested in tokio guide) to `LineCodec`. --- src/proto/line.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/proto/line.rs b/src/proto/line.rs index 2e1ade2..2c88d7e 100644 --- a/src/proto/line.rs +++ b/src/proto/line.rs @@ -12,13 +12,14 @@ use error; /// A line-based codec parameterized by an encoding. pub struct LineCodec { encoding: EncodingRef, + next_index: usize, } impl LineCodec { /// Creates a new instance of LineCodec from the specified encoding. pub fn new(label: &str) -> error::Result { encoding_from_whatwg_label(label) - .map(|enc| LineCodec { encoding: enc }) + .map(|enc| LineCodec { encoding: enc, next_index: 0 }) .ok_or_else(|| io::Error::new( io::ErrorKind::InvalidInput, &format!("Attempted to use unknown codec {}.", label)[..], @@ -31,9 +32,12 @@ impl Decoder for LineCodec { type Error = error::IrcError; fn decode(&mut self, src: &mut BytesMut) -> error::Result> { - if let Some(n) = src.as_ref().iter().position(|b| *b == b'\n') { + if let Some(offset) = src[self.next_index..].iter().position(|b| *b == b'\n') { // Remove the next frame from the buffer. - let line = src.split_to(n + 1); + let line = src.split_to(self.next_index + offset + 1); + + // Set the search start index back to 0 since we found a newline. + self.next_index = 0; // Decode the line using the codec's encoding. match self.encoding.decode(line.as_ref(), DecoderTrap::Replace) { @@ -46,6 +50,9 @@ impl Decoder for LineCodec { ), } } else { + // Set the search start index to the current length since we know that none of the + // characters we've already looked at are newlines. + self.next_index = src.len(); Ok(None) } } From 3aba7250b0cab774b497a230847e8c55d754b62d Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Tue, 10 Jul 2018 03:36:18 +0200 Subject: [PATCH 34/50] Moved Travis CI bot to pdgn.co. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index be06bf3..a7e0d6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ notifications: email: false irc: channels: - - "irc.fyrechat.net#vana-commits" + - "ircs://irc.pdgn.co:6697/#commits" template: - "%{repository_slug}/%{branch} (%{commit} - %{author}): %{message}" skip_join: true From b5b035faa75f79c63bb49fc71d907813216c214b Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Tue, 10 Jul 2018 04:15:08 +0200 Subject: [PATCH 35/50] Switched all the examples to use #rust-spam on irc.mozilla.org. --- examples/multiserver.rs | 8 ++++---- examples/reactor.rs | 4 ++-- examples/reconnector.rs | 8 ++++---- examples/simple.rs | 4 ++-- examples/simple_ssl.rs | 4 ++-- examples/tooter.rs | 6 +++--- examples/tweeter.rs | 6 +++--- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/multiserver.rs b/examples/multiserver.rs index c88311e..2e3bc27 100644 --- a/examples/multiserver.rs +++ b/examples/multiserver.rs @@ -7,15 +7,15 @@ use irc::client::prelude::*; fn main() { let cfg1 = Config { nickname: Some("pickles".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; let cfg2 = Config { nickname: Some("bananas".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; diff --git a/examples/reactor.rs b/examples/reactor.rs index 66dea6b..1a0d2b0 100644 --- a/examples/reactor.rs +++ b/examples/reactor.rs @@ -8,8 +8,8 @@ fn main() { let config = Config { nickname: Some("pickles".to_owned()), alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; diff --git a/examples/reconnector.rs b/examples/reconnector.rs index f879f48..da6fc8b 100644 --- a/examples/reconnector.rs +++ b/examples/reconnector.rs @@ -7,15 +7,15 @@ use irc::client::prelude::*; fn main() { let cfg1 = Config { nickname: Some("pickles".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; let cfg2 = Config { nickname: Some("bananas".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; diff --git a/examples/simple.rs b/examples/simple.rs index f6a621b..83bb777 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -7,8 +7,8 @@ fn main() { let config = Config { nickname: Some("pickles".to_owned()), alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; diff --git a/examples/simple_ssl.rs b/examples/simple_ssl.rs index 86ce6f3..201935a 100644 --- a/examples/simple_ssl.rs +++ b/examples/simple_ssl.rs @@ -6,8 +6,8 @@ use irc::client::prelude::*; fn main() { let config = Config { nickname: Some("pickles".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), use_ssl: Some(true), ..Default::default() }; diff --git a/examples/tooter.rs b/examples/tooter.rs index 42a2ad8..409765c 100644 --- a/examples/tooter.rs +++ b/examples/tooter.rs @@ -10,8 +10,8 @@ use irc::error::IrcError; fn main() { let config = Config { nickname: Some("mastodon".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; @@ -43,7 +43,7 @@ fn main() { // 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("#irc-crate", "TOOT TOOT") + send_client.send_privmsg("#rust-spam", "AWOOOOOOOOOO") })); // Then, on the main thread, we finally run the reactor which blocks the program indefinitely. diff --git a/examples/tweeter.rs b/examples/tweeter.rs index 830ca0d..824fab3 100644 --- a/examples/tweeter.rs +++ b/examples/tweeter.rs @@ -9,8 +9,8 @@ use irc::client::prelude::*; fn main() { let config = Config { nickname: Some("pickles".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), + server: Some("irc.mozilla.org".to_owned()), + channels: Some(vec!["#rust-spam".to_owned()]), ..Default::default() }; let client = IrcClient::from_config(config).unwrap(); @@ -21,7 +21,7 @@ fn main() { client2.stream().map(|m| print!("{}", m)).wait().count(); }); loop { - client.send_privmsg("#irc-crate", "TWEET TWEET").unwrap(); + client.send_privmsg("#rust-spam", "TWEET TWEET").unwrap(); thread::sleep(Duration::new(10, 0)); } } From 1abed4c5521acc3b651086271e1a9874ab573e2f Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 8 Aug 2018 20:13:50 -0400 Subject: [PATCH 36/50] Removed some langauge that I don't like (simple, easy). --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 89a4b7b..d1f369d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [doc]: https://docs.rs/irc [rfc2812]: http://tools.ietf.org/html/rfc2812 -[ircv3.1]: http://ircv3.net/irc/3.1.html +[ircv3.1]: http://ircv3.net/irc/3.1.html [ircv3.2]: http://ircv3.net/irc/3.2.html "the irc crate" is a thread-safe and async-friendly IRC client library written in Rust. It's @@ -39,7 +39,7 @@ Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pu ## Getting Started -To start using the irc crate with cargo, you can simply add `irc = "0.13"` to your dependencies in +To start using the irc crate with cargo, you can add `irc = "0.13"` to your dependencies in your Cargo.toml file. The high-level API can be found in [`irc::client::prelude`][irc-prelude]. You'll find a number of examples to help you get started in `examples/`, throughout the documentation, and below. @@ -72,7 +72,7 @@ fn main() { 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); // And here we can do whatever we want with the messages. @@ -173,8 +173,8 @@ You can convert between different configuration formats with `convertconf` like cargo run --example convertconf -- -i client_config.json -o client_config.toml ``` -Note that the formats are automatically determined based on the selected file extensions. This -tool should make it easy for users to migrate their old configurations to TOML. +Note that the formats are automatically determined based on the selected file extensions. This +tool should make it easier for users to migrate their old configurations to TOML. ## Contributing the irc crate is a free, open source library that relies on contributions from its maintainers, From abbf1eafcb29ce1253ffac5d251e338ed2b01843 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Thu, 16 Aug 2018 12:18:05 -0400 Subject: [PATCH 37/50] Added notes for PRIVMSG and NOTICE about responses (re: #144). --- src/proto/command.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/proto/command.rs b/src/proto/command.rs index f0ec551..022e886 100644 --- a/src/proto/command.rs +++ b/src/proto/command.rs @@ -49,8 +49,32 @@ pub enum Command { // 3.3 Sending messages /// PRIVMSG msgtarget :message + /// + /// ## Responding to a `PRIVMSG` + /// + /// When responding to a message, it is not sufficient to simply copy the message target + /// (msgtarget). This will work just fine for responding to messages in channels where the + /// target is the same for all participants. However, when the message is sent directly to a + /// user, this target will be that client's username, and responding to that same target will + /// actually mean sending itself a response. In such a case, you should instead respond to the + /// user sending the message as specified in the message prefix. Since this is a common + /// pattern, there is a utility function + /// [`Message::response_target`](../message/struct.Message.html#method.response_target) + /// which is used for this exact purpose. PRIVMSG(String, String), /// NOTICE msgtarget :message + /// + /// ## Responding to a `NOTICE` + /// + /// When responding to a notice, it is not sufficient to simply copy the message target + /// (msgtarget). This will work just fine for responding to messages in channels where the + /// target is the same for all participants. However, when the message is sent directly to a + /// user, this target will be that client's username, and responding to that same target will + /// actually mean sending itself a response. In such a case, you should instead respond to the + /// user sending the message as specified in the message prefix. Since this is a common + /// pattern, there is a utility function + /// [`Message::response_target`](../message/struct.Message.html#method.response_target) + /// which is used for this exact purpose. NOTICE(String, String), // 3.4 Server queries and commands From 6f2820d7f68b6396fd1324ce4c6dea19e90c2be9 Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 08:45:15 +0900 Subject: [PATCH 38/50] Upgrade log to 0.4 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 61165ae..65c5486 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ chrono = "0.4" encoding = "0.2" failure = "0.1" futures = "0.1" -log = "0.3" +log = "0.4" native-tls = "0.1" serde = "1.0" serde_derive = "1.0" From 12003c880ca7f8fec8ca320470a220fc447e7118 Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 08:50:38 +0900 Subject: [PATCH 39/50] Remove deprecated AsciiExt --- src/client/mod.rs | 2 -- src/proto/command.rs | 1 - 2 files changed, 3 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 0a05f56..258c94b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -45,8 +45,6 @@ //! # } //! ``` -#[cfg(feature = "ctcp")] -use std::ascii::AsciiExt; use std::collections::HashMap; use std::path::Path; use std::sync::{Arc, Mutex, RwLock}; diff --git a/src/proto/command.rs b/src/proto/command.rs index 022e886..10af3ea 100644 --- a/src/proto/command.rs +++ b/src/proto/command.rs @@ -1,5 +1,4 @@ //! Enumeration of all available client commands. -use std::ascii::AsciiExt; use std::str::FromStr; use error::MessageParseError; From 1cf77441560bab4c789e59cb38471c4b68838960 Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 08:54:22 +0900 Subject: [PATCH 40/50] Clarify to prevent unintended recursions --- src/proto/colors.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index 6ffb492..ba2afb3 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -102,10 +102,10 @@ impl FormattedStringExt for str { impl FormattedStringExt for String { fn is_formatted(&self) -> bool { - (&self).is_formatted() + (&self[..]).is_formatted() } fn strip_formatting(&self) -> Cow { - (&self).strip_formatting() + (&self[..]).strip_formatting() } } From 8ffac6bef05415efa90c02e6b43a1ef069914cf5 Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 09:18:50 +0900 Subject: [PATCH 41/50] Replace deprecated tokio_io::codec to tokio_codec --- Cargo.toml | 1 + src/client/conn.rs | 11 +++++++---- src/client/transport.rs | 2 +- src/lib.rs | 1 + src/proto/irc.rs | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65c5486..b39910c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ serde = "1.0" serde_derive = "1.0" serde_json = { version = "1.0", optional = true } serde_yaml = { version = "0.7", optional = true } +tokio-codec = "0.1" tokio-core = "0.1" tokio-io = "0.1" tokio-mockstream = "1.1" diff --git a/src/client/conn.rs b/src/client/conn.rs index e9552d2..1d90230 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -7,9 +7,9 @@ use encoding::EncoderTrap; use encoding::label::encoding_from_whatwg_label; use futures::{Async, Poll, Future, Sink, StartSend, Stream}; use native_tls::{Certificate, TlsConnector, Pkcs12}; +use tokio_codec::Decoder; use tokio_core::reactor::Handle; use tokio_core::net::{TcpStream, TcpStreamNew}; -use tokio_io::AsyncRead; use tokio_mockstream::MockStream; use tokio_tls::{TlsConnectorExt, TlsStream}; @@ -81,13 +81,15 @@ impl<'a> Future for ConnectionFuture<'a> { fn poll(&mut self) -> Poll { match *self { ConnectionFuture::Unsecured(config, ref mut inner) => { - let framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?); + let stream = try_ready!(inner.poll()); + let framed = IrcCodec::new(config.encoding())?.framed(stream); let transport = IrcTransport::new(config, framed); Ok(Async::Ready(Connection::Unsecured(transport))) } ConnectionFuture::Secured(config, ref mut inner) => { - let framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?); + let stream = try_ready!(inner.poll()); + let framed = IrcCodec::new(config.encoding())?.framed(stream); let transport = IrcTransport::new(config, framed); Ok(Async::Ready(Connection::Secured(transport))) @@ -109,7 +111,8 @@ impl<'a> Future for ConnectionFuture<'a> { }) }; - let framed = MockStream::new(&initial?).framed(IrcCodec::new(config.encoding())?); + let stream = MockStream::new(&initial?); + let framed = IrcCodec::new(config.encoding())?.framed(stream); let transport = IrcTransport::new(config, framed); Ok(Async::Ready(Connection::Mock(Logged::wrap(transport)))) diff --git a/src/client/transport.rs b/src/client/transport.rs index 39dc2e5..a05f594 100644 --- a/src/client/transport.rs +++ b/src/client/transport.rs @@ -6,8 +6,8 @@ use std::time::{Duration, Instant}; use futures::{Async, AsyncSink, Future, Poll, Sink, StartSend, Stream}; use chrono::prelude::*; +use tokio_codec::Framed; use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_io::codec::Framed; use tokio_timer; use tokio_timer::{Interval, Sleep, Timer}; diff --git a/src/lib.rs b/src/lib.rs index 3c9d32d..ed5f542 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,7 @@ extern crate serde_derive; extern crate serde_json; #[cfg(feature = "yaml")] extern crate serde_yaml; +extern crate tokio_codec; extern crate tokio_core; extern crate tokio_io; extern crate tokio_mockstream; diff --git a/src/proto/irc.rs b/src/proto/irc.rs index e10ae27..e87c322 100644 --- a/src/proto/irc.rs +++ b/src/proto/irc.rs @@ -1,6 +1,6 @@ //! Implementation of IRC codec for Tokio. use bytes::BytesMut; -use tokio_io::codec::{Decoder, Encoder}; +use tokio_codec::{Decoder, Encoder}; use error; use proto::line::LineCodec; From c7c65ef671852e872d7d7202db5b1fb906aebfa7 Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 10:39:20 +0900 Subject: [PATCH 42/50] Fix infinite recursive calls It reverts #137. --- src/proto/colors.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index ba2afb3..8e20e33 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -102,10 +102,10 @@ impl FormattedStringExt for str { impl FormattedStringExt for String { fn is_formatted(&self) -> bool { - (&self[..]).is_formatted() + self.as_str().is_formatted() } fn strip_formatting(&self) -> Cow { - (&self[..]).strip_formatting() + self.as_str().strip_formatting() } } @@ -117,6 +117,10 @@ mod test { fn test_strip_bold() { assert_eq!("l\x02ol".strip_formatting(), "lol"); } + #[test] + fn test_strip_bold_from_string() { + assert_eq!(String::from("l\x02ol").strip_formatting(), "lol"); + } #[test] fn test_strip_fg_color() { From 4feafc681a581373fb6954a8eb5d635573e632fc Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 10:58:36 +0900 Subject: [PATCH 43/50] Reduce memcpy case in strip_formatting This makes the return type of FormattedStringExt::strip_formatting more meaningful. Drawbacks: In theory, it breaks backward compatbility due to the shape of FormattedStringExt trait was changed. But I expected it's not the problem for the most case because it's rare to implement that trait while using this library. --- src/proto/colors.rs | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index 8e20e33..6836073 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -15,18 +15,18 @@ struct Parser { } /// An extension trait giving strings a function to strip IRC colors -pub trait FormattedStringExt { +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; + fn strip_formatting(self) -> Cow<'a, str>; } -impl FormattedStringExt for str { +impl<'a> FormattedStringExt<'a> for &'a str { fn is_formatted(&self) -> bool { self.contains('\x02') || // bold self.contains('\x1F') || // underline @@ -35,12 +35,22 @@ impl FormattedStringExt for str { self.contains('\x03') // color } - fn strip_formatting(&self) -> Cow { + fn strip_formatting(self) -> Cow<'a, str> { + if !self.is_formatted() { + return Cow::Borrowed(self); + } + Cow::Owned(internal::strip_formatting(self)) + } +} + +mod internal { // to reduce commit diff + use super::*; + pub(super) fn strip_formatting(input: &str) -> String { let mut parser = Parser { state: ParserState::Text, }; let mut prev: char = '\x00'; - let result: Cow = self + input .chars() .filter(move |cur| { let result = match parser.state { @@ -92,25 +102,25 @@ impl FormattedStringExt for str { prev = *cur; return result }) - .collect(); - - result + .collect() } - - } -impl FormattedStringExt for String { +impl FormattedStringExt<'static> for String { fn is_formatted(&self) -> bool { self.as_str().is_formatted() } - fn strip_formatting(&self) -> Cow { - self.as_str().strip_formatting() + fn strip_formatting(self) -> Cow<'static, str> { + if !self.is_formatted() { + return Cow::Owned(self); + } + Cow::Owned(internal::strip_formatting(&self)) } } #[cfg(test)] mod test { + use std::borrow::Cow; use proto::colors::FormattedStringExt; #[test] @@ -164,4 +174,13 @@ mod test { fn test_strip_string_with_digit_after_2digit_color() { assert_eq!("\x031212\x031111\x031010".strip_formatting(), "121110"); } + + #[test] + fn test_strip_no_allocation_for_unformatted_text() { + if let Cow::Borrowed(formatted) = "plain text".strip_formatting() { + assert_eq!(formatted, "plain text"); + } else { + panic!("allocation detected"); + } + } } From b108c833bb5b7b12ce59b9569d818d6e34fe16cf Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 11:28:45 +0900 Subject: [PATCH 44/50] Reduce allocation more String::strip_formatting will do most of operations in place. --- src/proto/colors.rs | 123 +++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index 6836073..c53470f 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -39,82 +39,79 @@ impl<'a> FormattedStringExt<'a> for &'a str { if !self.is_formatted() { return Cow::Borrowed(self); } - Cow::Owned(internal::strip_formatting(self)) + let mut s = String::from(self); + strip_formatting(&mut s); + Cow::Owned(s) } } -mod internal { // to reduce commit diff - use super::*; - pub(super) fn strip_formatting(input: &str) -> String { - let mut parser = Parser { - state: ParserState::Text, +fn strip_formatting(buf: &mut String) { + let mut parser = Parser { + state: ParserState::Text, + }; + let mut prev: char = '\x00'; + buf.retain(|cur| { + let result = match parser.state { + ParserState::Text | ParserState::Foreground1 | ParserState::Foreground2 if cur == '\x03' => { + parser.state = ParserState::ColorCode; + false + }, + ParserState::Text => !['\x02', '\x1F', '\x16', '\x0F'].contains(&cur), + ParserState::ColorCode if cur.is_digit(10) => { + parser.state = ParserState::Foreground1; + false + }, + ParserState::Foreground1 if cur.is_digit(6) => { + // can only consume another digit if previous char was 1. + if prev == '1' { + parser.state = ParserState::Foreground2; + false + } else { + parser.state = ParserState::Text; + true + } + }, + ParserState::Foreground1 if cur == ',' => { + parser.state = ParserState::Comma; + false + }, + ParserState::Foreground2 if cur == ',' => { + parser.state = ParserState::Comma; + false + }, + ParserState::Comma if (cur.is_digit(10)) => { + parser.state = ParserState::Background1; + false + }, + ParserState::Background1 if cur.is_digit(6) => { + // can only consume another digit if previous char was 1. + parser.state = ParserState::Text; + if prev == '1' { + false + } else { + true + } + } + _ => { + parser.state = ParserState::Text; + true + } }; - let mut prev: char = '\x00'; - input - .chars() - .filter(move |cur| { - let result = match parser.state { - ParserState::Text | ParserState::Foreground1 | ParserState::Foreground2 if *cur == '\x03' => { - parser.state = ParserState::ColorCode; - false - }, - ParserState::Text => !['\x02', '\x1F', '\x16', '\x0F'].contains(cur), - ParserState::ColorCode if (*cur).is_digit(10) => { - parser.state = ParserState::Foreground1; - false - }, - ParserState::Foreground1 if (*cur).is_digit(6) => { - // can only consume another digit if previous char was 1. - if (prev) == '1' { - parser.state = ParserState::Foreground2; - false - } else { - parser.state = ParserState::Text; - true - } - }, - ParserState::Foreground1 if *cur == ',' => { - parser.state = ParserState::Comma; - false - }, - ParserState::Foreground2 if *cur == ',' => { - parser.state = ParserState::Comma; - false - }, - ParserState::Comma if ((*cur).is_digit(10)) => { - parser.state = ParserState::Background1; - false - }, - ParserState::Background1 if (*cur).is_digit(6) => { - // can only consume another digit if previous char was 1. - parser.state = ParserState::Text; - if (prev) == '1' { - false - } else { - true - } - } - _ => { - parser.state = ParserState::Text; - true - } - }; - prev = *cur; - return result - }) - .collect() - } + prev = cur; + return result + }); } impl FormattedStringExt<'static> for String { fn is_formatted(&self) -> bool { self.as_str().is_formatted() } - fn strip_formatting(self) -> Cow<'static, str> { + fn strip_formatting(mut self) -> Cow<'static, str> { if !self.is_formatted() { return Cow::Owned(self); } - Cow::Owned(internal::strip_formatting(&self)) + strip_formatting(&mut self); + Cow::Owned(self) } } From e7fe08f29dce6a718c4f337710b6377c5fd0dce6 Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 11:38:34 +0900 Subject: [PATCH 45/50] Make is_formatted simple --- src/proto/colors.rs | 88 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index c53470f..58c32bf 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -25,14 +25,17 @@ pub trait FormattedStringExt<'a> { } +const FORMAT_CHARACTERS: &[char] = &[ + '\x02', // bold + '\x1F', // underline + '\x16', // reverse + '\x0F', // normal + '\x03', // color +]; impl<'a> FormattedStringExt<'a> for &'a str { fn is_formatted(&self) -> bool { - self.contains('\x02') || // bold - self.contains('\x1F') || // underline - self.contains('\x16') || // reverse - self.contains('\x0F') || // normal - self.contains('\x03') // color + self.contains(FORMAT_CHARACTERS) } fn strip_formatting(self) -> Cow<'a, str> { @@ -56,7 +59,7 @@ fn strip_formatting(buf: &mut String) { parser.state = ParserState::ColorCode; false }, - ParserState::Text => !['\x02', '\x1F', '\x16', '\x0F'].contains(&cur), + ParserState::Text => !FORMAT_CHARACTERS.contains(&cur), ParserState::ColorCode if cur.is_digit(10) => { parser.state = ParserState::Foreground1; false @@ -120,6 +123,79 @@ mod test { use std::borrow::Cow; use proto::colors::FormattedStringExt; + #[test] + fn test_is_formatted_blank() { + assert!(!"".is_formatted()); + } + #[test] + fn test_is_formatted_blank2() { + assert!(!" ".is_formatted()); + } + #[test] + fn test_is_formatted_blank3() { + assert!(!"\t\r\n".is_formatted()); + } + #[test] + fn test_is_formatted_bold() { + assert!("l\x02ol".is_formatted()); + } + #[test] + fn test_is_formatted_fg_color() { + assert!("l\x033ol".is_formatted()); + } + #[test] + fn test_is_formatted_fg_color2() { + assert!("l\x0312ol".is_formatted()); + } + #[test] + fn test_is_formatted_fg_bg_11() { + assert!("l\x031,2ol".is_formatted()); + } + #[test] + fn test_is_formatted_fg_bg_21() { + assert!("l\x0312,3ol".is_formatted()); + } + #[test] + fn test_is_formatted_fg_bg_12() { + assert!("l\x031,12ol".is_formatted()); + } + #[test] + fn test_is_formatted_fg_bg_22() { + assert!("l\x0312,13ol".is_formatted()); + } + #[test] + fn test_is_formatted_string_with_multiple_colors() { + assert!("hoo\x034r\x033a\x0312y".is_formatted()); + } + #[test] + fn test_is_formatted_string_with_digit_after_color() { + assert!("\x0344\x0355\x0366".is_formatted()); + } + #[test] + fn test_is_formatted_string_with_multiple_2digit_colors() { + assert!("hoo\x0310r\x0311a\x0312y".is_formatted()); + } + #[test] + fn test_is_formatted_string_with_digit_after_2digit_color() { + assert!("\x031212\x031111\x031010".is_formatted()); + } + #[test] + fn test_is_formatted_unformatted() { + assert!(!"a plain text".is_formatted()); + } + + #[test] + fn test_strip_blank() { + assert_eq!("".strip_formatting(), ""); + } + #[test] + fn test_strip_blank2() { + assert_eq!(" ".strip_formatting(), " "); + } + #[test] + fn test_strip_blank3() { + assert_eq!("\t\r\n".strip_formatting(), "\t\r\n"); + } #[test] fn test_strip_bold() { assert_eq!("l\x02ol".strip_formatting(), "lol"); From c311d37902caf0750f3100b8dabed859317a8c8e Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 12:14:25 +0900 Subject: [PATCH 46/50] Use macro --- src/proto/colors.rs | 169 +++++++++++++------------------------------- 1 file changed, 48 insertions(+), 121 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index 58c32bf..c8eb052 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -123,129 +123,56 @@ mod test { use std::borrow::Cow; use proto::colors::FormattedStringExt; - #[test] - fn test_is_formatted_blank() { - assert!(!"".is_formatted()); - } - #[test] - fn test_is_formatted_blank2() { - assert!(!" ".is_formatted()); - } - #[test] - fn test_is_formatted_blank3() { - assert!(!"\t\r\n".is_formatted()); - } - #[test] - fn test_is_formatted_bold() { - assert!("l\x02ol".is_formatted()); - } - #[test] - fn test_is_formatted_fg_color() { - assert!("l\x033ol".is_formatted()); - } - #[test] - fn test_is_formatted_fg_color2() { - assert!("l\x0312ol".is_formatted()); - } - #[test] - fn test_is_formatted_fg_bg_11() { - assert!("l\x031,2ol".is_formatted()); - } - #[test] - fn test_is_formatted_fg_bg_21() { - assert!("l\x0312,3ol".is_formatted()); - } - #[test] - fn test_is_formatted_fg_bg_12() { - assert!("l\x031,12ol".is_formatted()); - } - #[test] - fn test_is_formatted_fg_bg_22() { - assert!("l\x0312,13ol".is_formatted()); - } - #[test] - fn test_is_formatted_string_with_multiple_colors() { - assert!("hoo\x034r\x033a\x0312y".is_formatted()); - } - #[test] - fn test_is_formatted_string_with_digit_after_color() { - assert!("\x0344\x0355\x0366".is_formatted()); - } - #[test] - fn test_is_formatted_string_with_multiple_2digit_colors() { - assert!("hoo\x0310r\x0311a\x0312y".is_formatted()); - } - #[test] - fn test_is_formatted_string_with_digit_after_2digit_color() { - assert!("\x031212\x031111\x031010".is_formatted()); - } - #[test] - fn test_is_formatted_unformatted() { - assert!(!"a plain text".is_formatted()); + macro_rules! test_formatted_string_ext { + { $( $name:ident ( $($line:tt)* ), )* } => { + $( + mod $name { + use super::*; + test_formatted_string_ext!(@ $($line)*); + } + )* + }; + (@ $text:expr, should stripped into $expected:expr) => { + #[test] + fn test_formatted() { + assert!($text.is_formatted()); + } + #[test] + fn test_strip() { + assert_eq!($text.strip_formatting(), $expected); + } + }; + (@ $text:expr, is not formatted) => { + #[test] + fn test_formatted() { + assert!(!$text.is_formatted()); + } + #[test] + fn test_strip() { + assert_eq!($text.strip_formatting(), $text); + } + } } - #[test] - fn test_strip_blank() { - assert_eq!("".strip_formatting(), ""); - } - #[test] - fn test_strip_blank2() { - assert_eq!(" ".strip_formatting(), " "); - } - #[test] - fn test_strip_blank3() { - assert_eq!("\t\r\n".strip_formatting(), "\t\r\n"); - } - #[test] - fn test_strip_bold() { - assert_eq!("l\x02ol".strip_formatting(), "lol"); - } - #[test] - fn test_strip_bold_from_string() { - assert_eq!(String::from("l\x02ol").strip_formatting(), "lol"); - } - - #[test] - fn test_strip_fg_color() { - assert_eq!("l\x033ol".strip_formatting(), "lol"); - } - - #[test] - fn test_strip_fg_color2() { - assert_eq!("l\x0312ol".strip_formatting(), "lol"); - } - - #[test] - fn test_strip_fg_bg_11() { - assert_eq!("l\x031,2ol".strip_formatting(), "lol"); - } - #[test] - fn test_strip_fg_bg_21() { - assert_eq!("l\x0312,3ol".strip_formatting(), "lol"); - } - #[test] - fn test_strip_fg_bg_12() { - assert_eq!("l\x031,12ol".strip_formatting(), "lol"); - } - #[test] - fn test_strip_fg_bg_22() { - assert_eq!("l\x0312,13ol".strip_formatting(), "lol"); - } - #[test] - fn test_strip_string_with_multiple_colors() { - assert_eq!("hoo\x034r\x033a\x0312y".strip_formatting(), "hooray"); - } - #[test] - fn test_strip_string_with_digit_after_color() { - assert_eq!("\x0344\x0355\x0366".strip_formatting(), "456"); - } - #[test] - fn test_strip_string_with_multiple_2digit_colors() { - assert_eq!("hoo\x0310r\x0311a\x0312y".strip_formatting(), "hooray"); - } - #[test] - fn test_strip_string_with_digit_after_2digit_color() { - assert_eq!("\x031212\x031111\x031010".strip_formatting(), "121110"); + test_formatted_string_ext! { + blank("", is not formatted), + blank2(" ", is not formatted), + blank3("\t\r\n", is not formatted), + bold("l\x02ol", should stripped into "lol"), + bold_from_string(String::from("l\x02ol"), should stripped into "lol"), + bold_hangul("우왕\x02굳", should stripped into "우왕굳"), + fg_color("l\x033ol", should stripped into "lol"), + fg_color2("l\x0312ol", should stripped into "lol"), + fg_bg_11("l\x031,2ol", should stripped into "lol"), + fg_bg_21("l\x0312,3ol", should stripped into "lol"), + fg_bg_12("l\x031,12ol", should stripped into "lol"), + fg_bg_22("l\x0312,13ol", should stripped into "lol"), + string_with_multiple_colors("hoo\x034r\x033a\x0312y", should stripped into "hooray"), + string_with_digit_after_color("\x0344\x0355\x0366", should stripped into "456"), + string_with_multiple_2digit_colors("hoo\x0310r\x0311a\x0312y", should stripped into "hooray"), + string_with_digit_after_2digit_color("\x031212\x031111\x031010", should stripped into "121110"), + thinking("🤔...", is not formatted), + unformatted("a plain text", is not formatted), } #[test] From 8d07c117bb149fb8e3bf3e6ea6213c2054176b1b Mon Sep 17 00:00:00 2001 From: Eunchong Yu Date: Sun, 19 Aug 2018 12:36:16 +0900 Subject: [PATCH 47/50] Polish the parser --- src/proto/colors.rs | 108 ++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/src/proto/colors.rs b/src/proto/colors.rs index c8eb052..d70fb12 100644 --- a/src/proto/colors.rs +++ b/src/proto/colors.rs @@ -1,14 +1,13 @@ //! An extension trait that provides the ability to strip IRC colors from a string use std::borrow::Cow; -#[derive(PartialEq)] enum ParserState { Text, ColorCode, - Foreground1, + Foreground1(char), Foreground2, Comma, - Background1, + Background1(char), } struct Parser { state: ParserState, @@ -49,60 +48,63 @@ impl<'a> FormattedStringExt<'a> for &'a str { } fn strip_formatting(buf: &mut String) { - let mut parser = Parser { - state: ParserState::Text, - }; - let mut prev: char = '\x00'; - buf.retain(|cur| { - let result = match parser.state { - ParserState::Text | ParserState::Foreground1 | ParserState::Foreground2 if cur == '\x03' => { - parser.state = ParserState::ColorCode; + let mut parser = Parser::new(); + buf.retain(|cur| parser.next(cur)); +} + +impl Parser { + fn new() -> Self { + Parser { + state: ParserState::Text, + } + } + + fn next(&mut self, cur: char) -> bool { + use self::ParserState::*; + match self.state { + Text | Foreground1(_) | Foreground2 if cur == '\x03' => { + self.state = ColorCode; false - }, - ParserState::Text => !FORMAT_CHARACTERS.contains(&cur), - ParserState::ColorCode if cur.is_digit(10) => { - parser.state = ParserState::Foreground1; - false - }, - ParserState::Foreground1 if cur.is_digit(6) => { - // can only consume another digit if previous char was 1. - if prev == '1' { - parser.state = ParserState::Foreground2; - false - } else { - parser.state = ParserState::Text; - true - } - }, - ParserState::Foreground1 if cur == ',' => { - parser.state = ParserState::Comma; - false - }, - ParserState::Foreground2 if cur == ',' => { - parser.state = ParserState::Comma; - false - }, - ParserState::Comma if (cur.is_digit(10)) => { - parser.state = ParserState::Background1; - false - }, - ParserState::Background1 if cur.is_digit(6) => { - // can only consume another digit if previous char was 1. - parser.state = ParserState::Text; - if prev == '1' { - false - } else { - true - } } - _ => { - parser.state = ParserState::Text; + Text => { + !FORMAT_CHARACTERS.contains(&cur) + } + ColorCode if cur.is_digit(10) => { + self.state = Foreground1(cur); + false + } + Foreground1('1') if cur.is_digit(6) => { + // can only consume another digit if previous char was 1. + self.state = Foreground2; + false + } + Foreground1(_) if cur.is_digit(6) => { + self.state = Text; true } - }; - prev = cur; - return result - }); + Foreground1(_) if cur == ',' => { + self.state = Comma; + false + } + Foreground2 if cur == ',' => { + self.state = Comma; + false + } + Comma if (cur.is_digit(10)) => { + self.state = Background1(cur); + false + } + Background1(prev) if cur.is_digit(6) => { + // can only consume another digit if previous char was 1. + self.state = Text; + prev != '1' + } + _ => { + self.state = Text; + true + } + } + } } impl FormattedStringExt<'static> for String { From 0be5cb26c876f8ac0033616b4b09a48920101e7c Mon Sep 17 00:00:00 2001 From: Jokler Date: Mon, 17 Sep 2018 02:11:25 +0200 Subject: [PATCH 48/50] Upgrade native-tls and tokio-tls to 0.2 --- Cargo.toml | 4 ++-- src/client/conn.rs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b39910c..ec2db0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ encoding = "0.2" failure = "0.1" futures = "0.1" log = "0.4" -native-tls = "0.1" +native-tls = "0.2" serde = "1.0" serde_derive = "1.0" serde_json = { version = "1.0", optional = true } @@ -40,7 +40,7 @@ tokio-core = "0.1" tokio-io = "0.1" tokio-mockstream = "1.1" tokio-timer = "0.1" -tokio-tls = "0.1" +tokio-tls = "0.2" toml = { version = "0.4", optional = true } [dev-dependencies] diff --git a/src/client/conn.rs b/src/client/conn.rs index 1d90230..3c8ea22 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -6,12 +6,12 @@ use std::io::Read; use encoding::EncoderTrap; use encoding::label::encoding_from_whatwg_label; use futures::{Async, Poll, Future, Sink, StartSend, Stream}; -use native_tls::{Certificate, TlsConnector, Pkcs12}; +use native_tls::{Certificate, TlsConnector, Identity}; use tokio_codec::Decoder; use tokio_core::reactor::Handle; use tokio_core::net::{TcpStream, TcpStreamNew}; use tokio_mockstream::MockStream; -use tokio_tls::{TlsConnectorExt, TlsStream}; +use tokio_tls::{self, TlsStream}; use error; use client::data::Config; @@ -129,13 +129,13 @@ impl Connection { } else if config.use_ssl() { let domain = format!("{}", config.server()?); info!("Connecting via SSL to {}.", domain); - let mut builder = TlsConnector::builder()?; + let mut builder = TlsConnector::builder(); if let Some(cert_path) = config.cert_path() { let mut file = File::open(cert_path)?; let mut cert_data = vec![]; file.read_to_end(&mut cert_data)?; let cert = Certificate::from_der(&cert_data)?; - builder.add_root_certificate(cert)?; + builder.add_root_certificate(cert); info!("Added {} to trusted certificates.", cert_path); } if let Some(client_cert_path) = config.client_cert_path() { @@ -143,16 +143,16 @@ impl Connection { let mut file = File::open(client_cert_path)?; let mut client_cert_data = vec![]; file.read_to_end(&mut client_cert_data)?; - let pkcs12_archive = Pkcs12::from_der(&client_cert_data, &client_cert_pass)?; - builder.identity(pkcs12_archive)?; + let pkcs12_archive = Identity::from_pkcs12(&client_cert_data, &client_cert_pass)?; + builder.identity(pkcs12_archive); info!("Using {} for client certificate authentication.", client_cert_path); } - let connector = builder.build()?; + let connector: tokio_tls::TlsConnector = builder.build()?.into(); let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle).map_err(|e| { let res: error::IrcError = e.into(); res }).and_then(move |socket| { - connector.connect_async(&domain, socket).map_err( + connector.connect(&domain, socket).map_err( |e| e.into(), ) })); From 16a59a96f87261f1f3d3bd7c2b2315956312e11b Mon Sep 17 00:00:00 2001 From: nuxeh Date: Wed, 3 Oct 2018 16:00:18 +0100 Subject: [PATCH 49/50] Add a link to url-bot-rs Add a link to url-bot-rs, a project making use of this crate, to the readme. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d1f369d..d06b900 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ projects: - [irc-bot.rs][ircbot] — a library for writing IRC bots - [playbot_ng][playbot_ng] — a Rust-evaluating IRC bot in Rust - [bunnybutt-rs][bunnybutt] — an IRC bot for the [Feed The Beast Wiki][ftb-wiki] +- [url-bot-rs][url-bot-rs] — a URL-fetching IRC bot [alectro]: https://github.com/aatxe/alectro [spilo]: https://github.com/aatxe/spilo @@ -34,6 +35,7 @@ projects: [bunnybutt]: https://github.com/FTB-Gamepedia/bunnybutt-rs [playbot_ng]: https://github.com/panicbit/playbot_ng [ftb-wiki]: https://ftb.gamepedia.com/FTB_Wiki +[url-bot-rs]: https://github.com/nuxeh/url-bot-rs Making your own project? [Submit a pull request](https://github.com/aatxe/irc/pulls) to add it! From cf9a93127a2187faf20859779e8c75dcf48b4def Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 3 Oct 2018 11:34:37 -0400 Subject: [PATCH 50/50] Bumped version number to 0.13.6. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ec2db0c..606a501 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "irc" -version = "0.13.5" +version = "0.13.6" description = "the irc crate – usable, async IRC for Rust " authors = ["Aaron Weiss "] license = "MPL-2.0"