From bbc6b0244d8e678e9156921340242e098cdfe3ca Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Mon, 8 Jan 2018 21:52:56 -0500 Subject: [PATCH 01/31] Added an experimental reactor API to hide tokio. --- examples/nothreads.rs | 24 +++++--------------- src/client/mod.rs | 2 ++ src/client/reactor.rs | 52 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 src/client/reactor.rs diff --git a/examples/nothreads.rs b/examples/nothreads.rs index 9185186..fbec2e1 100644 --- a/examples/nothreads.rs +++ b/examples/nothreads.rs @@ -1,12 +1,8 @@ -extern crate futures; extern crate irc; -extern crate tokio_core; use std::default::Default; -use futures::future; use irc::error; use irc::client::prelude::*; -use tokio_core::reactor::Core; fn main() { let cfg1 = Config { @@ -17,31 +13,23 @@ fn main() { }; let cfg2 = Config { - nickname: Some("pickles".to_owned()), - server: Some("irc.pdgn.co".to_owned()), + nickname: Some("bananas".to_owned()), + server: Some("irc.fyrechat.net".to_owned()), channels: Some(vec!["#irc-crate".to_owned()]), - use_ssl: Some(true), ..Default::default() }; let configs = vec![cfg1, cfg2]; - // Create an event loop to run the multiple connections on. - let mut reactor = Core::new().unwrap(); - let processor_handle = reactor.handle(); + let mut reactor = IrcReactor::new().unwrap(); for config in configs { - let handle = reactor.handle(); - let server = reactor.run(IrcServer::new_future(handle, &config).unwrap()).unwrap(); + let server = reactor.prepare_server_and_connect(&config).unwrap(); server.identify().unwrap(); - - processor_handle.spawn(server.stream().for_each(move |message| { - process_msg(&server, message) - }).map_err(|e| Err(e).unwrap())) + reactor.register_server_with_handler(server, process_msg); } - // You might instead want to join all the futures and run them directly. - reactor.run(future::empty::<(), ()>()).unwrap(); + reactor.run().unwrap(); } fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { diff --git a/src/client/mod.rs b/src/client/mod.rs index 45085e7..4ab8f92 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -2,6 +2,7 @@ pub mod conn; pub mod data; +pub mod reactor; pub mod server; pub mod transport; @@ -29,6 +30,7 @@ pub mod prelude { //! as well as in the parsed form of received mode commands. pub use client::data::Config; + pub use client::reactor::IrcReactor; pub use client::server::{EachIncomingExt, IrcServer, Server}; pub use client::server::utils::ServerExt; pub use proto::{Capability, ChannelExt, Command, Message, NegotiationVersion, Response}; diff --git a/src/client/reactor.rs b/src/client/reactor.rs new file mode 100644 index 0000000..3817cc2 --- /dev/null +++ b/src/client/reactor.rs @@ -0,0 +1,52 @@ +//! A system for creating and managing multiple connections to IRC servers. + +use client::data::Config; +use client::server::{IrcServer, IrcServerFuture, Server}; +use error; +use proto::Message; + +use futures::{Future, Stream}; +use futures::future; +use tokio_core::reactor::Core; + +pub struct IrcReactor { + inner: Core, + handlers: Vec>>, +} + +impl IrcReactor { + pub fn new() -> error::Result { + Ok(IrcReactor { + inner: Core::new()?, + handlers: Vec::new(), + }) + } + + pub fn prepare_server<'a>(&mut self, config: &'a Config) -> error::Result> { + IrcServer::new_future(self.inner.handle(), config) + } + + pub fn connect_server(&mut self, future: IrcServerFuture) -> error::Result { + self.inner.run(future) + } + + pub fn prepare_server_and_connect(&mut self, config: &Config) -> error::Result { + self.prepare_server(config).and_then(|future| self.connect_server(future)) + } + + pub fn register_server_with_handler( + &mut self, server: IrcServer, handler: F + ) where F: Fn(&IrcServer, Message) -> error::Result<()> + 'static { + self.handlers.push(Box::new(server.stream().for_each(move |message| { + handler(&server, message) + }))); + } + + pub fn run(&mut self) -> error::Result<()> { + let mut handlers = Vec::new(); + while let Some(handler) = self.handlers.pop() { + handlers.push(handler); + } + self.inner.run(future::join_all(handlers).map(|_| ())) + } +} From a7ae091ff5ec758e0d976b3969701f72b712406e Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Mon, 8 Jan 2018 23:19:29 -0500 Subject: [PATCH 02/31] Moved connection creation sooner for better error handling in IrcServer::from_config. --- src/client/server/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 7e6104b..166ac05 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -700,16 +700,16 @@ impl IrcServer { let (tx_incoming, rx_incoming) = oneshot::channel(); let (tx_view, rx_view) = oneshot::channel(); - let cfg = config.clone(); + let mut reactor = Core::new()?; + let handle = reactor.handle(); + // Attempting to connect here (as opposed to on the thread) allows more errors to happen + // immediately, rather than to occur as panics on the thread. In particular, non-resolving + // server names, and failed SSL setups will appear here. + let conn = reactor.run(Connection::new(&config, &handle)?)?; + let _ = thread::spawn(move || { let mut reactor = Core::new().unwrap(); - // Setting up internal processing stuffs. - let handle = reactor.handle(); - let conn = reactor - .run(Connection::new(&cfg, &handle).unwrap()) - .unwrap(); - tx_view.send(conn.log_view()).unwrap(); let (sink, stream) = conn.split(); From c3863ed76b7c53824db7d970dbd238a71e07ccb5 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Mon, 8 Jan 2018 23:42:03 -0500 Subject: [PATCH 03/31] Changed Travis CI badge to display status of stable branch. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b0d781..53f420a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # irc [![Build Status][ci-badge]][ci] [![Crates.io][cr-badge]][cr] [![Docs][doc-badge]][doc] [![Built with Spacemacs][bws]][sm] -[ci-badge]: https://travis-ci.org/aatxe/irc.svg?branch=master +[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 From 47bbf38497263277aae95ea7982c0ff56403a9ac Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Mon, 8 Jan 2018 23:50:41 -0500 Subject: [PATCH 04/31] Removed nothreads_alt because that behavior is the default with reactors. --- examples/nothreads_alt.rs | 62 --------------------------------------- 1 file changed, 62 deletions(-) delete mode 100644 examples/nothreads_alt.rs diff --git a/examples/nothreads_alt.rs b/examples/nothreads_alt.rs deleted file mode 100644 index f0bd5cd..0000000 --- a/examples/nothreads_alt.rs +++ /dev/null @@ -1,62 +0,0 @@ -extern crate futures; -extern crate irc; -extern crate tokio_core; - -use std::default::Default; -use futures::future; -use irc::error; -use irc::client::prelude::*; -use tokio_core::reactor::Core; - -fn main() { - let cfg1 = Config { - nickname: Some("pickles1".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), - ..Default::default() - }; - - let cfg2 = Config { - nickname: Some("pickles2".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), - ..Default::default() - }; - - let configs = vec![cfg1, cfg2]; - - let (futures, mut reactor) = configs.iter().fold( - (vec![], Core::new().unwrap()), - |(mut acc, mut reactor), config| { - let handle = reactor.handle(); - // First, we run the future representing the connection to the server. - // After this is complete, we have connected and can send and receive messages. - let server = reactor.run(IrcServer::new_future(handle, config).unwrap()).unwrap(); - server.identify().unwrap(); - - // Add the future for processing messages from the current server to the accumulator. - acc.push(server.stream().for_each(move |message| { - process_msg(&server, message) - })); - - // We then thread through the updated accumulator and the reactor. - (acc, reactor) - } - ); - - // Here, we join on all of the futures representing the message handling for each server. - reactor.run(future::join_all(futures)).unwrap(); -} - -fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { - print!("{}", message); - match message.command { - Command::PRIVMSG(ref target, ref msg) => { - if msg.contains("pickles") { - server.send_privmsg(target, "Hi!")?; - } - } - _ => (), - } - Ok(()) -} From fc4b4cad7f92a208a2541171fbda30c05f12635a Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Mon, 8 Jan 2018 23:54:06 -0500 Subject: [PATCH 05/31] Reorganized imports in reactor.rs. --- src/client/reactor.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/reactor.rs b/src/client/reactor.rs index 3817cc2..ee8efdd 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -1,14 +1,14 @@ //! A system for creating and managing multiple connections to IRC servers. +use futures::{Future, Stream}; +use futures::future; +use tokio_core::reactor::Core; + use client::data::Config; use client::server::{IrcServer, IrcServerFuture, Server}; use error; use proto::Message; -use futures::{Future, Stream}; -use futures::future; -use tokio_core::reactor::Core; - pub struct IrcReactor { inner: Core, handlers: Vec>>, From 233eab6969a60c67e5f0654e2c66c16d8e62b511 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Tue, 9 Jan 2018 15:02:57 -0500 Subject: [PATCH 06/31] Added an example that reconnects on error using IrcReactor. --- examples/reconnector.rs | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/reconnector.rs diff --git a/examples/reconnector.rs b/examples/reconnector.rs new file mode 100644 index 0000000..b3270b5 --- /dev/null +++ b/examples/reconnector.rs @@ -0,0 +1,58 @@ +extern crate irc; + +use std::default::Default; +use irc::error; +use irc::client::prelude::*; + +fn main() { + let cfg1 = Config { + nickname: Some("pickles".to_owned()), + server: Some("irc.fyrechat.net".to_owned()), + channels: Some(vec!["#irc-crate".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()]), + ..Default::default() + }; + + let configs = vec![cfg1, cfg2]; + + let mut reactor = IrcReactor::new().unwrap(); + + loop { + let res = configs.iter().fold(Ok(()), |acc, config| { + acc.and( + reactor.prepare_server_and_connect(config).and_then(|server| { + server.identify().and(Ok(server)) + }).and_then(|server| { + reactor.register_server_with_handler(server, process_msg); + Ok(()) + }) + ) + }).and_then(|()| reactor.run()); + + match res { + Ok(_) => break, + Err(e) => eprintln!("{}", e), + } + } +} + +fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { + print!("{}", message); + match message.command { + Command::PRIVMSG(ref target, ref msg) => { + if msg.contains("pickles") { + server.send_privmsg(target, "Hi!")?; + } else if msg.contains("quit") { + server.send_quit("bye")?; + } + } + _ => (), + } + Ok(()) +} From ab16c865bb8f1680d6c93198b241419cafa0e9aa Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 24 Jan 2018 12:13:33 +0100 Subject: [PATCH 07/31] Added a function to spawn arbitrary futures on a reactor (i.e. opt-in "full" tokio support). --- src/client/reactor.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/reactor.rs b/src/client/reactor.rs index ee8efdd..b255009 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -42,6 +42,12 @@ impl IrcReactor { }))); } + pub fn register_future( + &mut self, future: F + ) where F: Future + 'static { + self.handlers.push(Box::new(future)) + } + pub fn run(&mut self) -> error::Result<()> { let mut handlers = Vec::new(); while let Some(handler) = self.handlers.pop() { From fbe17978fcf0bd9cf4a37f85b4ee5107aae3b10e Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 24 Jan 2018 12:21:03 +0100 Subject: [PATCH 08/31] Removed trailing whitespace in documentation. --- src/client/server/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 7e6104b..36cc634 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -17,7 +17,7 @@ //! use irc::client::prelude::{IrcServer, ServerExt}; //! //! # fn main() { -//! let server = IrcServer::new("config.toml").unwrap(); +//! let server = IrcServer::new("config.toml").unwrap(); //! // identify comes from `ServerExt` //! server.identify().unwrap(); //! # } @@ -85,7 +85,7 @@ pub mod utils; /// use irc::client::prelude::EachIncomingExt; /// /// # fn main() { -/// # let server = IrcServer::new("config.toml").unwrap(); +/// # let server = IrcServer::new("config.toml").unwrap(); /// # server.identify().unwrap(); /// server.stream().for_each_incoming(|irc_msg| { /// match irc_msg.command { @@ -128,7 +128,7 @@ pub trait Server { /// # extern crate irc; /// # use irc::client::prelude::*; /// # fn main() { - /// # let server = IrcServer::new("config.toml").unwrap(); + /// # let server = IrcServer::new("config.toml").unwrap(); /// server.send(Command::NICK("example".to_owned())).unwrap(); /// server.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap(); /// # } @@ -153,7 +153,7 @@ pub trait Server { /// # extern crate irc; /// # use irc::client::prelude::{IrcServer, ServerExt, Server, Command}; /// # fn main() { - /// # let server = IrcServer::new("config.toml").unwrap(); + /// # let server = IrcServer::new("config.toml").unwrap(); /// # server.identify().unwrap(); /// server.for_each_incoming(|irc_msg| { /// match irc_msg.command { @@ -189,7 +189,7 @@ pub trait Server { /// use irc::proto::caps::Capability; /// /// # fn main() { - /// # let server = IrcServer::new("config.toml").unwrap(); + /// # let server = IrcServer::new("config.toml").unwrap(); /// server.send_cap_req(&[Capability::MultiPrefix]).unwrap(); /// server.identify().unwrap(); /// # } @@ -667,7 +667,7 @@ impl IrcServer { /// # extern crate irc; /// # use irc::client::prelude::*; /// # fn main() { - /// let server = IrcServer::new("config.toml").unwrap(); + /// let server = IrcServer::new("config.toml").unwrap(); /// # } /// ``` pub fn new>(config: P) -> error::Result { @@ -691,7 +691,7 @@ impl IrcServer { /// server: Some("irc.example.com".to_owned()), /// .. Default::default() /// }; - /// let server = IrcServer::from_config(config).unwrap(); + /// let server = IrcServer::from_config(config).unwrap(); /// # } /// ``` pub fn from_config(config: Config) -> error::Result { @@ -755,7 +755,7 @@ impl IrcServer { /// # .. Default::default() /// # }; /// let mut reactor = Core::new().unwrap(); - /// let future = IrcServer::new_future(reactor.handle(), &config).unwrap(); + /// let future = IrcServer::new_future(reactor.handle(), &config).unwrap(); /// // immediate connection errors (like no internet) will turn up here... /// let server = reactor.run(future).unwrap(); /// // runtime errors (like disconnections and so forth) will turn up here... From 495e419de0193e66e2b4305de6dbe424b8a84281 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 24 Jan 2018 12:28:07 +0100 Subject: [PATCH 09/31] Added some documentation to the reactor examples. --- examples/nothreads.rs | 5 +++++ examples/reconnector.rs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/examples/nothreads.rs b/examples/nothreads.rs index fbec2e1..b513946 100644 --- a/examples/nothreads.rs +++ b/examples/nothreads.rs @@ -24,11 +24,16 @@ fn main() { let mut reactor = IrcReactor::new().unwrap(); for config in configs { + // Immediate errors like failure to resolve the server's name or to establish any connection will + // manifest here in the result of prepare_server_and_connect. let server = reactor.prepare_server_and_connect(&config).unwrap(); server.identify().unwrap(); + // Here, we tell the reactor to setup this server for future handling (in run) using the specified + // handler function process_msg. reactor.register_server_with_handler(server, process_msg); } + // Runtime errors like a dropped connection will manifest here in the result of run. reactor.run().unwrap(); } diff --git a/examples/reconnector.rs b/examples/reconnector.rs index b3270b5..0a29be5 100644 --- a/examples/reconnector.rs +++ b/examples/reconnector.rs @@ -36,7 +36,9 @@ fn main() { }).and_then(|()| reactor.run()); match res { + // The connections ended normally (for example, they sent a QUIT message to the server). Ok(_) => break, + // Something went wrong! We'll print the error, and restart the connections. Err(e) => eprintln!("{}", e), } } From e62d4a97aa8d0a5e5152c2ac1d785107bdf00fc1 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Wed, 24 Jan 2018 14:51:41 +0100 Subject: [PATCH 10/31] Added a first cut (without examples) of documentation for IrcReactor. --- src/client/reactor.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/client/reactor.rs b/src/client/reactor.rs index b255009..0d4847e 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -1,4 +1,4 @@ -//! A system for creating and managing multiple connections to IRC servers. +//! A system for creating and managing IRC server connections. use futures::{Future, Stream}; use futures::future; @@ -15,6 +15,7 @@ pub struct IrcReactor { } impl IrcReactor { + /// Creates a new reactor. pub fn new() -> error::Result { Ok(IrcReactor { inner: Core::new()?, @@ -22,18 +23,29 @@ impl IrcReactor { }) } + /// Creates a representation of an IRC server that has not yet attempted to connect. In + /// particular, this representation is as a Future that when run will produce a connected + /// [IrcServer](./server/struct.IrcServer.html). pub fn prepare_server<'a>(&mut self, config: &'a Config) -> error::Result> { IrcServer::new_future(self.inner.handle(), config) } + /// Runs an [IrcServerFuture](./server/struct.IrcServerFuture.html), such as one from + /// `prepare_server` to completion, yielding an [IrcServer](./server/struct.IrcServer.html). pub fn connect_server(&mut self, future: IrcServerFuture) -> error::Result { self.inner.run(future) } + /// Creates a new IRC server from the specified configuration, connecting immediately. This is + /// guaranteed to be the composition of prepare_server and connect_server. pub fn prepare_server_and_connect(&mut self, config: &Config) -> error::Result { self.prepare_server(config).and_then(|future| self.connect_server(future)) } + /// Registers the given server with the specified message handler. The reactor will store this + /// setup until the next call to run, where it will be used to process new messages over the + /// connection indefinitely (or until failure). As registration is consumed by `run`, subsequent + /// calls to run will require new registration. pub fn register_server_with_handler( &mut self, server: IrcServer, handler: F ) where F: Fn(&IrcServer, Message) -> error::Result<()> + 'static { @@ -42,12 +54,19 @@ impl IrcReactor { }))); } + /// Registers an arbitrary future with this reactor. This is a sort of escape hatch that allows + /// you to take more control over what runs on the reactor without requiring you to bring in + /// additional knowledge about `tokio`. It is suspected that `register_server_with_handler` will + /// be sufficient for most use cases. pub fn register_future( &mut self, future: F ) where F: Future + 'static { self.handlers.push(Box::new(future)) } + /// Consumes all registered handlers and futures, and runs them. When using + /// `register_server_with_handler`, this will block indefinitely (until failure occurs) as it + /// will simply continue to process new, incoming messages for each server that was registered. pub fn run(&mut self) -> error::Result<()> { let mut handlers = Vec::new(); while let Some(handler) = self.handlers.pop() { From 982d1b5a0d7580cf4723ea45b210dfa5ae712ad6 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 15:18:15 +0100 Subject: [PATCH 11/31] Added a function to access the tokio reactor handle in IrcReactor. --- src/client/reactor.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client/reactor.rs b/src/client/reactor.rs index 0d4847e..54d75d2 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -2,7 +2,7 @@ use futures::{Future, Stream}; use futures::future; -use tokio_core::reactor::Core; +use tokio_core::reactor::{Core, Handle}; use client::data::Config; use client::server::{IrcServer, IrcServerFuture, Server}; @@ -64,6 +64,13 @@ impl IrcReactor { self.handlers.push(Box::new(future)) } + /// Returns a handle to the internal event loop. This is a sort of escape hatch that allows you + /// to take more control over what runs on the reactor using `tokio`. This can be used for + /// sharing this reactor with some elements of other libraries. + pub fn inner_handle(&self) -> Handle { + self.inner.handle() + } + /// Consumes all registered handlers and futures, and runs them. When using /// `register_server_with_handler`, this will block indefinitely (until failure occurs) as it /// will simply continue to process new, incoming messages for each server that was registered. From ff9eba272254a474da7db80f3649b06398013105 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 15:35:44 +0100 Subject: [PATCH 12/31] Made some of the reactor API more generic. --- src/client/reactor.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/client/reactor.rs b/src/client/reactor.rs index 54d75d2..c5875c9 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -1,6 +1,6 @@ //! A system for creating and managing IRC server connections. -use futures::{Future, Stream}; +use futures::{Future, IntoFuture, Stream}; use futures::future; use tokio_core::reactor::{Core, Handle}; @@ -46,9 +46,10 @@ impl IrcReactor { /// setup until the next call to run, where it will be used to process new messages over the /// connection indefinitely (or until failure). As registration is consumed by `run`, subsequent /// calls to run will require new registration. - pub fn register_server_with_handler( + pub fn register_server_with_handler( &mut self, server: IrcServer, handler: F - ) where F: Fn(&IrcServer, Message) -> error::Result<()> + 'static { + ) where F: Fn(&IrcServer, Message) -> U + 'static, + U: IntoFuture + 'static { self.handlers.push(Box::new(server.stream().for_each(move |message| { handler(&server, message) }))); @@ -60,8 +61,8 @@ impl IrcReactor { /// be sufficient for most use cases. pub fn register_future( &mut self, future: F - ) where F: Future + 'static { - self.handlers.push(Box::new(future)) + ) where F: IntoFuture + 'static { + self.handlers.push(Box::new(future.into_future())) } /// Returns a handle to the internal event loop. This is a sort of escape hatch that allows you From fd4e5706eb57c8ad110560a847a973d31096c576 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 19:26:48 +0100 Subject: [PATCH 13/31] Added more documentation for IrcReactor, and made new_future internal-only. --- src/client/mod.rs | 3 +- src/client/reactor.rs | 102 +++++++++++++++++++++++++++++++++++++++ src/client/server/mod.rs | 7 ++- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 4ab8f92..b7287fd 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -17,7 +17,8 @@ pub mod prelude { //! method to send `Commands` because it makes it easy to see the whole set of possible //! interactions with a server. The `ServerExt` trait addresses this deficiency by defining a //! number of methods that provide a more clear and succinct interface for sending various - //! common IRC commands to the server. + //! common IRC commands to the server. An `IrcReactor` can be used to create and manage multiple + //! `IrcServers` with more fine-grained control over error management. //! //! The various `proto` types capture details of the IRC protocol that are used throughout the //! client API. `Message`, `Command`, and `Response` are used to send and receive messages along diff --git a/src/client/reactor.rs b/src/client/reactor.rs index c5875c9..bd54033 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -1,4 +1,27 @@ //! A system for creating and managing IRC server connections. +//! +//! This API provides the ability to create and manage multiple IRC servers that can run on the same +//! thread through the use of a shared event loop. It also replaces the old functionality of +//! `IrcServer::new_future` and better encapsulates dependencies on `tokio` and `futures`. Finally, +//! it provides some escape hatches that let advanced users take advantage of these dependencies +//! regardless. +//! +//! # Example +//! ```no_run +//! # extern crate irc; +//! # use std::default::Default; +//! use irc::client::prelude::*; +//! use irc::error; +//! +//! fn main() { +//! let config = Config::default(); +//! let mut reactor = IrcReactor::new().unwrap(); +//! let server = reactor.prepare_server_and_connect(&config).unwrap(); +//! reactor.register_server_with_handler(server, process_msg); +//! reactor.run().unwrap(); +//! } +//! # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) } +//! ``` use futures::{Future, IntoFuture, Stream}; use futures::future; @@ -9,6 +32,13 @@ use client::server::{IrcServer, IrcServerFuture, Server}; use error; use proto::Message; +/// A thin wrapper over an event loop. +/// +/// An IRC reactor is used to create new connections to IRC servers and to drive the management of +/// all connected servers as the application runs. It can be used to run multiple servers on the +/// same thread, as well as to get better control over error management in an IRC client. +/// +/// For a full example usage, see [irc::client::reactor](./index.html). pub struct IrcReactor { inner: Core, handlers: Vec>>, @@ -26,18 +56,59 @@ impl IrcReactor { /// Creates a representation of an IRC server that has not yet attempted to connect. In /// particular, this representation is as a Future that when run will produce a connected /// [IrcServer](./server/struct.IrcServer.html). + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use std::default::Default; + /// # use irc::client::prelude::*; + /// # fn main() { + /// # let config = Config::default(); + /// let future_server = IrcReactor::new().and_then(|mut reactor| { + /// reactor.prepare_server(&config) + /// }); + /// # } + /// ``` pub fn prepare_server<'a>(&mut self, config: &'a Config) -> error::Result> { IrcServer::new_future(self.inner.handle(), config) } /// Runs an [IrcServerFuture](./server/struct.IrcServerFuture.html), such as one from /// `prepare_server` to completion, yielding an [IrcServer](./server/struct.IrcServer.html). + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use std::default::Default; + /// # use irc::client::prelude::*; + /// # fn main() { + /// # let config = Config::default(); + /// let server = IrcReactor::new().and_then(|mut reactor| { + /// reactor.prepare_server(&config).and_then(|future| { + /// reactor.connect_server(future) + /// }) + /// }); + /// # } + /// ``` pub fn connect_server(&mut self, future: IrcServerFuture) -> error::Result { self.inner.run(future) } /// Creates a new IRC server from the specified configuration, connecting immediately. This is /// guaranteed to be the composition of prepare_server and connect_server. + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use std::default::Default; + /// # use irc::client::prelude::*; + /// # fn main() { + /// # let config = Config::default(); + /// let server = IrcReactor::new().and_then(|mut reactor| { + /// reactor.prepare_server_and_connect(&config) + /// }); + /// # } + /// ``` pub fn prepare_server_and_connect(&mut self, config: &Config) -> error::Result { self.prepare_server(config).and_then(|future| self.connect_server(future)) } @@ -46,6 +117,22 @@ impl IrcReactor { /// setup until the next call to run, where it will be used to process new messages over the /// connection indefinitely (or until failure). As registration is consumed by `run`, subsequent /// calls to run will require new registration. + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use std::default::Default; + /// # use irc::client::prelude::*; + /// # fn main() { + /// # let config = Config::default(); + /// let mut reactor = IrcReactor::new().unwrap(); + /// let server = reactor.prepare_server_and_connect(&config).unwrap(); + /// reactor.register_server_with_handler(server, |server, msg| { + /// // Message processing happens here. + /// Ok(()) + /// }) + /// # } + /// ``` pub fn register_server_with_handler( &mut self, server: IrcServer, handler: F ) where F: Fn(&IrcServer, Message) -> U + 'static, @@ -75,6 +162,21 @@ impl IrcReactor { /// Consumes all registered handlers and futures, and runs them. When using /// `register_server_with_handler`, this will block indefinitely (until failure occurs) as it /// will simply continue to process new, incoming messages for each server that was registered. + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use std::default::Default; + /// # use irc::client::prelude::*; + /// # use irc::error; + /// # fn main() { + /// # let config = Config::default(); + /// let mut reactor = IrcReactor::new().unwrap(); + /// let server = reactor.prepare_server_and_connect(&config).unwrap(); + /// reactor.register_server_with_handler(server, process_msg) + /// # } + /// # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) } + /// ``` pub fn run(&mut self) -> error::Result<()> { let mut handlers = Vec::new(); while let Some(handler) = self.handlers.pop() { diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 36cc634..c48d394 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -594,6 +594,8 @@ impl ServerState { /// server after connection. Cloning an `IrcServer` is relatively cheap, as it's equivalent to /// cloning a single `Arc`. This may be useful for setting up multiple threads with access to one /// connection. +/// +/// For a full example usage, see [irc::client::server](./index.html). #[derive(Clone)] pub struct IrcServer { /// The internal, thread-safe server state. @@ -678,7 +680,8 @@ impl IrcServer { /// immediately. Due to current design limitations, error handling here is somewhat limited. In /// particular, failed connections will cause the program to panic because the connection /// attempt is made on a freshly created thread. If you need to avoid this behavior and handle - /// errors more gracefully, it is recommended that you use `IrcServer::new_future` instead. + /// errors more gracefully, it is recommended that you use an + /// [IrcReactor](../reactor/struct.IrcReactor.html) instead. /// /// # Example /// ```no_run @@ -766,7 +769,7 @@ impl IrcServer { /// # } /// # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) } /// ``` - pub fn new_future(handle: Handle, config: &Config) -> error::Result { + pub(crate) fn new_future(handle: Handle, config: &Config) -> error::Result { let (tx_outgoing, rx_outgoing) = mpsc::unbounded(); Ok(IrcServerFuture { From 67cca339a76b2cc9ad0027cd3636d97c4729ec18 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 19:50:48 +0100 Subject: [PATCH 14/31] Eliminated a panic that can occur when using IrcServerFuture, and recommend using IrcReactor instead. --- src/client/reactor.rs | 17 ++++++++++------- src/client/server/mod.rs | 36 +++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/client/reactor.rs b/src/client/reactor.rs index bd54033..48e9aca 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -1,10 +1,10 @@ //! A system for creating and managing IRC server connections. //! //! This API provides the ability to create and manage multiple IRC servers that can run on the same -//! thread through the use of a shared event loop. It also replaces the old functionality of -//! `IrcServer::new_future` and better encapsulates dependencies on `tokio` and `futures`. Finally, -//! it provides some escape hatches that let advanced users take advantage of these dependencies -//! regardless. +//! thread through the use of a shared event loop. It can also be used to encapsulate the dependency +//! on `tokio` and `futures` in the use of `IrcServer::new_future`. This means that knowledge of +//! those libraries should be unnecessary for the average user. Nevertheless, this API also provides +//! some escape hatches that let advanced users take further advantage of these dependencies. //! //! # Example //! ```no_run @@ -28,7 +28,7 @@ use futures::future; use tokio_core::reactor::{Core, Handle}; use client::data::Config; -use client::server::{IrcServer, IrcServerFuture, Server}; +use client::server::{IrcServer, IrcServerFuture, PackedIrcServer, Server}; use error; use proto::Message; @@ -70,7 +70,7 @@ impl IrcReactor { /// # } /// ``` pub fn prepare_server<'a>(&mut self, config: &'a Config) -> error::Result> { - IrcServer::new_future(self.inner.handle(), config) + IrcServer::new_future(self.inner_handle(), config) } /// Runs an [IrcServerFuture](./server/struct.IrcServerFuture.html), such as one from @@ -91,7 +91,10 @@ impl IrcReactor { /// # } /// ``` pub fn connect_server(&mut self, future: IrcServerFuture) -> error::Result { - self.inner.run(future) + self.inner.run(future).map(|PackedIrcServer(server, future)| { + self.register_future(future); + server + }) } /// Creates a new IRC server from the specified configuration, connecting immediately. This is diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index c48d394..f0356f9 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -741,7 +741,9 @@ impl IrcServer { /// Proper usage requires familiarity with `tokio` and `futures`. You can find more information /// in the crate documentation for [tokio-core](http://docs.rs/tokio-core) or /// [futures](http://docs.rs/futures). Additionally, you can find detailed tutorials on using - /// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). + /// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). An easy + /// to use abstraction that does not require this knowledge is available via + /// [IrcReactors](../reactor/struct.IrcReactor.html). /// /// # Example /// ```no_run @@ -749,6 +751,7 @@ impl IrcServer { /// # extern crate tokio_core; /// # use std::default::Default; /// # use irc::client::prelude::*; + /// # use irc::client::server::PackedIrcServer; /// # use irc::error; /// # use tokio_core::reactor::Core; /// # fn main() { @@ -760,21 +763,21 @@ impl IrcServer { /// let mut reactor = Core::new().unwrap(); /// let future = IrcServer::new_future(reactor.handle(), &config).unwrap(); /// // immediate connection errors (like no internet) will turn up here... - /// let server = reactor.run(future).unwrap(); + /// let PackedIrcServer(server, future) = reactor.run(future).unwrap(); /// // runtime errors (like disconnections and so forth) will turn up here... /// reactor.run(server.stream().for_each(move |irc_msg| { /// // processing messages works like usual /// process_msg(&server, irc_msg) - /// })).unwrap(); + /// }).join(future)).unwrap(); /// # } /// # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) } /// ``` - pub(crate) fn new_future(handle: Handle, config: &Config) -> error::Result { + pub fn new_future(handle: Handle, config: &Config) -> error::Result { let (tx_outgoing, rx_outgoing) = mpsc::unbounded(); Ok(IrcServerFuture { conn: Connection::new(config, &handle)?, - handle: handle, + _handle: handle, config: config, tx_outgoing: Some(tx_outgoing), rx_outgoing: Some(rx_outgoing), @@ -800,17 +803,18 @@ impl IrcServer { /// Interaction with this future relies on the `futures` API, but is only expected for more advanced /// use cases. To learn more, you can view the documentation for the /// [futures](https://docs.rs/futures/) crate, or the tutorials for -/// [tokio](https://tokio.rs/docs/getting-started/futures/). +/// [tokio](https://tokio.rs/docs/getting-started/futures/). An easy to use abstraction that does +/// not require this knowledge is available via [IrcReactors](../reactor/struct.IrcReactor.html). pub struct IrcServerFuture<'a> { conn: ConnectionFuture<'a>, - handle: Handle, + _handle: Handle, config: &'a Config, tx_outgoing: Option>, rx_outgoing: Option>, } impl<'a> Future for IrcServerFuture<'a> { - type Item = IrcServer; + type Item = PackedIrcServer; type Error = error::Error; fn poll(&mut self) -> Poll { @@ -822,19 +826,25 @@ impl<'a> Future for IrcServerFuture<'a> { let outgoing_future = sink.send_all(self.rx_outgoing.take().unwrap().map_err(|()| { let res: error::Error = error::ErrorKind::ChannelError.into(); res - })).map(|_| ()).map_err(|e| panic!(e)); + })).map(|_| ()); - self.handle.spawn(outgoing_future); - - Ok(Async::Ready(IrcServer { + let server = IrcServer { state: Arc::new(ServerState::new( stream, self.tx_outgoing.take().unwrap(), self.config.clone() )), view: view, - })) + }; + Ok(Async::Ready(PackedIrcServer(server, Box::new(outgoing_future)))) } } +/// An `IrcServer` packaged with a future that drives its message sending. In order for the server +/// to actually work properly, this future _must_ be running. +/// +/// This type should only be used by advanced users who are familiar with the implementation of this +/// crate. An easy to use abstraction that does not require this knowledge is available via +/// [IrcReactors](../reactor/struct.IrcReactor.html). +pub struct PackedIrcServer(pub IrcServer, pub Box>); #[cfg(test)] mod test { From ce158fc6129ee5c274c2c6e005bc7e9d8b494829 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 20:26:56 +0100 Subject: [PATCH 15/31] Made unspecified server or nickname into errors instead of simply panics. --- src/client/conn.rs | 4 ++-- src/client/data/config.rs | 17 ++++++++--------- src/client/server/mod.rs | 9 ++++++--- src/client/server/utils.rs | 2 +- src/error.rs | 11 ++++++++++- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/client/conn.rs b/src/client/conn.rs index 22548a6..106a539 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -108,7 +108,7 @@ impl Connection { if config.use_mock_connection() { Ok(ConnectionFuture::Mock(config)) } else if config.use_ssl() { - let domain = format!("{}", config.server()); + let domain = format!("{}", config.server()?); info!("Connecting via SSL to {}.", domain); let mut builder = TlsConnector::builder()?; if let Some(cert_path) = config.cert_path() { @@ -133,7 +133,7 @@ impl Connection { )); Ok(ConnectionFuture::Secured(config, stream)) } else { - info!("Connecting to {}.", config.server()); + info!("Connecting to {}.", config.server()?); Ok(ConnectionFuture::Unsecured( config, TcpStream::connect(&config.socket_addr()?, handle), diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 5833d29..2d34c3f 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -259,9 +259,8 @@ impl Config { } /// Gets the nickname specified in the configuration. - /// This will panic if not specified. - pub fn nickname(&self) -> &str { - self.nickname.as_ref().map(|s| &s[..]).unwrap() + pub fn nickname(&self) -> Result<&str> { + self.nickname.as_ref().map(|s| &s[..]).ok_or(error::ErrorKind::NicknameNotSpecified.into()) } /// Gets the bot's nickserv password specified in the configuration. @@ -282,19 +281,19 @@ impl Config { /// Gets the username specified in the configuration. /// This defaults to the user's nickname when not specified. pub fn username(&self) -> &str { - self.username.as_ref().map_or(self.nickname(), |s| &s) + self.username.as_ref().map_or(self.nickname().unwrap_or("user"), |s| &s) } /// Gets the real name specified in the configuration. /// This defaults to the user's nickname when not specified. pub fn real_name(&self) -> &str { - self.realname.as_ref().map_or(self.nickname(), |s| &s) + self.realname.as_ref().map_or(self.nickname().unwrap_or("irc"), |s| &s) } /// Gets the address of the server specified in the configuration. - /// This panics when not specified. - pub fn server(&self) -> &str { - self.server.as_ref().map(|s| &s[..]).unwrap() + pub fn server(&self) -> Result<&str> { + self.server.as_ref().map(|s| &s[..]).ok_or(error::ErrorKind::NicknameNotSpecified.into()) + } /// Gets the port of the server specified in the configuration. @@ -310,7 +309,7 @@ impl Config { /// Gets the server and port as a `SocketAddr`. /// This panics when server is not specified or the address is malformed. pub fn socket_addr(&self) -> Result { - format!("{}:{}", self.server(), self.port()).to_socket_addrs() + format!("{}:{}", self.server()?, self.port()).to_socket_addrs() .map(|mut i| i.next().unwrap()) .map_err(|e| e.into()) } diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 166ac05..4537fba 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -326,7 +326,9 @@ impl ServerState { let alt_nicks = self.config().alternate_nicknames(); let index = self.alt_nick_index.read().unwrap(); match *index { - 0 => self.config().nickname(), + 0 => self.config().nickname().expect( + "current_nickname should not be callable if nickname is not defined." + ), i => alt_nicks[i - 1], } } @@ -348,6 +350,7 @@ impl ServerState { trace!("[RECV] {}", msg.to_string()); match msg.command { JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan), + /// This will panic if not specified. PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan), QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")), NICK(ref new_nick) => { @@ -420,12 +423,12 @@ impl ServerState { self.send(NICKSERV(format!( "{} {} {}", seq, - self.config().nickname(), + self.config().nickname()?, self.config().nick_password() )))?; } *index = 0; - self.send(NICK(self.config().nickname().to_owned()))? + self.send(NICK(self.config().nickname()?.to_owned()))? } self.send(NICKSERV( format!("IDENTIFY {}", self.config().nick_password()), diff --git a/src/client/server/utils.rs b/src/client/server/utils.rs index e5cd779..8105c19 100644 --- a/src/client/server/utils.rs +++ b/src/client/server/utils.rs @@ -95,7 +95,7 @@ pub trait ServerExt: Server { if self.config().password() != "" { self.send(PASS(self.config().password().to_owned()))?; } - self.send(NICK(self.config().nickname().to_owned()))?; + self.send(NICK(self.config().nickname()?.to_owned()))?; self.send(USER( self.config().username().to_owned(), "0".to_owned(), diff --git a/src/error.rs b/src/error.rs index e68a99f..e4fea6b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,4 @@ //! Errors for `irc` crate using `error_chain`. - #![allow(missing_docs)] error_chain! { @@ -54,5 +53,15 @@ error_chain! { description("The connection timed out due to no ping response.") display("The connection timed out due to no ping response.") } + + NicknameNotSpecified { + description("No nickname was specified for use with this IrcServer.") + display("No nickname was specified for use with this IrcServer.") + } + + ServerNotSpecified { + description("No server was specified to connect to.") + display("No server was specified to connect to.") + } } } From de6e5f331d8fc4b8e1b725540d530f305dc72351 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 20:39:35 +0100 Subject: [PATCH 16/31] Added debug derivations for irc::client types (fixes #104). --- src/client/conn.rs | 19 +++++++++++++++++++ src/client/server/mod.rs | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/client/conn.rs b/src/client/conn.rs index 106a539..8dbdc0e 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -55,6 +55,25 @@ pub enum ConnectionFuture<'a> { Mock(&'a Config), } +impl<'a> fmt::Debug for ConnectionFuture<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}({:?}, ...)", + match *self { + ConnectionFuture::Unsecured(_, _) => "ConnectionFuture::Unsecured", + ConnectionFuture::Secured(_, _) => "ConnectionFuture::Secured", + ConnectionFuture::Mock(_) => "ConnectionFuture::Mock", + }, + match *self { + ConnectionFuture::Unsecured(cfg, _) | + ConnectionFuture::Secured(cfg, _) | + ConnectionFuture::Mock(cfg) => cfg, + } + ) + } +} + impl<'a> Future for ConnectionFuture<'a> { type Item = Connection; type Error = error::Error; diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 4537fba..db82895 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -203,6 +203,7 @@ pub trait Server { /// traditional use cases. To learn more, you can view the documentation for the /// [futures](https://docs.rs/futures/) crate, or the tutorials for /// [tokio](https://tokio.rs/docs/getting-started/futures/). +#[derive(Debug)] pub struct ServerStream { state: Arc, stream: SplitStream, @@ -224,6 +225,7 @@ impl Stream for ServerStream { } /// Thread-safe internal state for an IRC server connection. +#[derive(Debug)] struct ServerState { /// The configuration used with this connection. config: Config, @@ -597,7 +599,7 @@ impl ServerState { /// server after connection. Cloning an `IrcServer` is relatively cheap, as it's equivalent to /// cloning a single `Arc`. This may be useful for setting up multiple threads with access to one /// connection. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct IrcServer { /// The internal, thread-safe server state. state: Arc, @@ -801,6 +803,7 @@ impl IrcServer { /// use cases. To learn more, you can view the documentation for the /// [futures](https://docs.rs/futures/) crate, or the tutorials for /// [tokio](https://tokio.rs/docs/getting-started/futures/). +#[derive(Debug)] pub struct IrcServerFuture<'a> { conn: ConnectionFuture<'a>, handle: Handle, From 8349141b10f804db40929bbffc860553a7debe5a Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 20:48:49 +0100 Subject: [PATCH 17/31] Clarified scope of code of conduct further. --- CODE_OF_CONDUCT.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9e44bc3..737429b 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,4 +1,4 @@ -# Contributor Covenant Code of Conduct +# Code of Conduct ## Our Pledge @@ -50,13 +50,15 @@ when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +further defined and clarified by project maintainers. In general, non-maintainer +contributors will not be considered representing the project in public spaces +except when explicitly stating such. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [awe@pdgn.co](mailto:awe@pdgn.co). All -complaints will be reviewed and investigated and will result in a response that +reported by contacting the lead project maintainer at [awe@pdgn.co](mailto:awe@pdgn.co). +All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. From 6550bd0d96bf1248a1d04fff70a69ccd39c364b9 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 20:53:12 +0100 Subject: [PATCH 18/31] Specified where to find our code of conduct in README. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 53f420a..4019413 100644 --- a/README.md +++ b/README.md @@ -130,5 +130,4 @@ tool should make it easy for users to migrate their old configurations to TOML. Contributions to this library would be immensely appreciated. Prior to version 0.12.0, this library was public domain. As of 0.12.0, this library is offered under the Mozilla Public License 2.0 whose text can be found in `LICENSE.md`. Fostering an inclusive community around `irc` is -important, and to that end, we've adopted the -[Contributor Convenant](https://www.contributor-covenant.org). +important, and to that end, we've adopted an explicit Code of Conduct found in `CODE_OF_CONDUCT.md`. From 2d28a71c6c7a45c53300853c45de51d1125cddd1 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 20:59:41 +0100 Subject: [PATCH 19/31] Removed some trailing whitespace in reactor docs. --- 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 48e9aca..dca3fe9 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -12,13 +12,13 @@ //! # use std::default::Default; //! use irc::client::prelude::*; //! use irc::error; -//! +//! //! fn main() { //! let config = Config::default(); //! let mut reactor = IrcReactor::new().unwrap(); //! let server = reactor.prepare_server_and_connect(&config).unwrap(); //! reactor.register_server_with_handler(server, process_msg); -//! reactor.run().unwrap(); +//! reactor.run().unwrap(); //! } //! # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) } //! ``` From 518303a0134b1e6b187234f5769a346497f79fc5 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 21:10:22 +0100 Subject: [PATCH 20/31] Disabled JSON support by default. --- Cargo.toml | 2 +- README.md | 10 ++++------ mktestconfig.sh | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30bbfd6..ebef181 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ readme = "README.md" travis-ci = { repository = "aatxe/irc" } [features] -default = ["ctcp", "json", "toml"] +default = ["ctcp", "toml"] ctcp = [] nochanlists = [] json = ["serde_json"] diff --git a/README.md b/README.md index 4019413..39858ba 100644 --- a/README.md +++ b/README.md @@ -74,12 +74,10 @@ fn main() { Like the rest of the IRC crate, configuration is built with flexibility in mind. You can easily create `Config` objects programmatically and choose your own methods for handling any saving or loading of configuration required. However, for convenience, we've also included the option of -loading files with `serde` to write configurations. By default, we support JSON and TOML. As of -0.12.4, TOML is the preferred configuration format. We have bundled a conversion tool as -`convertconf` in the examples. In a future version, we will likely disable JSON by default. -Additionally, you can enable the optional `yaml` feature to get support for YAML as well. All the -configuration fields are optional, and thus any of them can be omitted (though, omitting a -nickname or server will cause the program to fail for obvious reasons). +loading files with `serde` to write configurations. The default configuration format is TOML, +though there is optional support for JSON and YAML via the optional `json` and `yaml` features. All +the configuration fields are optional, and can thus be omitted, but a working configuration requires +at least a `server` and `nickname`. Here's an example of a complete configuration in TOML: diff --git a/mktestconfig.sh b/mktestconfig.sh index b6f5fa8..f1d1d6e 100755 --- a/mktestconfig.sh +++ b/mktestconfig.sh @@ -1,3 +1,3 @@ -echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.test.net\",\"port\": 6667,\"use_ssl\": false,\"encoding\": \"UTF-8\",\"channels\": [\"#test\", \"#test2\"],\"umodes\": \"+BR\",\"options\": {}}" > client_config.json -cargo run --example convertconf --features "toml yaml" -- -i client_config.json -o client_config.toml -cargo run --example convertconf --features "toml yaml" -- -i client_config.json -o client_config.yaml +echo "{\"owners\": [\"test\"],\"nickname\": \"test\",\"username\": \"test\",\"realname\": \"test\",\"password\": \"\",\"server\": \"irc.test.net\",\"port\": 6667,\"use_ssl\": false,\"encoding\": \"UTF-8\",\"channels\": [\"#test\", \"#test2\"],\"umodes\": \"+BR\",\"options\": {}}" > client_config.json +cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.toml +cargo run --example convertconf --features "json yaml" -- -i client_config.json -o client_config.yaml From 2c543e967492ee682f0d347de02944ae064000ec Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sat, 27 Jan 2018 21:12:56 +0100 Subject: [PATCH 21/31] Added README pointer to configuration fields in the docs. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 39858ba..1fd7117 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,8 @@ loading of configuration required. However, for convenience, we've also included loading files with `serde` to write configurations. The default configuration format is TOML, though there is optional support for JSON and YAML via the optional `json` and `yaml` features. All the configuration fields are optional, and can thus be omitted, but a working configuration requires -at least a `server` and `nickname`. +at least a `server` and `nickname`. You can find detailed explanations of the configuration format +[here](https://docs.rs/irc/0.12.8/irc/client/data/config/struct.Config.html#fields). Here's an example of a complete configuration in TOML: From 5266e4098d5a84c73cad6fd76ca292914c74eeed Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 00:52:11 +0100 Subject: [PATCH 22/31] Refactored the whole crate to use failure. --- Cargo.toml | 2 +- examples/convertconf.rs | 2 +- src/client/conn.rs | 50 +++---- src/client/data/config.rs | 266 ++++++++++++++++++++---------------- src/client/reactor.rs | 6 +- src/client/server/mod.rs | 36 +++-- src/client/transport.rs | 26 ++-- src/error.rs | 276 ++++++++++++++++++++++++++++++-------- src/lib.rs | 3 +- src/proto/command.rs | 29 ++-- src/proto/irc.rs | 4 +- src/proto/line.rs | 4 +- src/proto/message.rs | 37 +++-- src/proto/mode.rs | 42 ++++-- 14 files changed, 513 insertions(+), 270 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ebef181..2d69270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ bufstream = "0.1" bytes = "0.4" chrono = "0.4" encoding = "0.2" -error-chain = "0.10" +failure = "0.1" futures = "0.1" log = "0.3" native-tls = "0.1" diff --git a/examples/convertconf.rs b/examples/convertconf.rs index 97f3b81..24af879 100644 --- a/examples/convertconf.rs +++ b/examples/convertconf.rs @@ -16,7 +16,7 @@ fn main() { let args: Vec<_> = env::args().collect(); match parse(&args) { Ok(Some((ref input, ref output))) => { - let cfg = Config::load(input).unwrap(); + let mut cfg = Config::load(input).unwrap(); cfg.save(output).unwrap(); println!("Converted {} to {}.", input, output); } diff --git a/src/client/conn.rs b/src/client/conn.rs index 8dbdc0e..56c0697 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -1,6 +1,6 @@ //! A module providing IRC connections for use by `IrcServer`s. use std::fs::File; -use std::{fmt, io}; +use std::fmt; use std::io::Read; use encoding::EncoderTrap; @@ -43,7 +43,7 @@ impl fmt::Debug for Connection { } /// A convenient type alias representing the `TlsStream` future. -type TlsFuture = Box> + Send>; +type TlsFuture = Box> + Send>; /// A future representing an eventual `Connection`. pub enum ConnectionFuture<'a> { @@ -76,7 +76,7 @@ impl<'a> fmt::Debug for ConnectionFuture<'a> { impl<'a> Future for ConnectionFuture<'a> { type Item = Connection; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll { match *self { @@ -95,21 +95,18 @@ impl<'a> Future for ConnectionFuture<'a> { ConnectionFuture::Mock(ref config) => { let enc: error::Result<_> = encoding_from_whatwg_label( config.encoding() - ).ok_or_else(|| io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Attempted to use unknown codec {}.", config.encoding())[..], - ).into()); + ).ok_or_else(|| error::IrcError::UnknownCodec { + codec: config.encoding().to_owned(), + }); let encoding = enc?; let init_str = config.mock_initial_value(); let initial: error::Result<_> = { - encoding.encode(init_str, EncoderTrap::Replace).map_err( - |data| { - io::Error::new( - io::ErrorKind::InvalidInput, - &format!("Failed to encode {} as {}.", data, encoding.name())[..], - ).into() - }, - ) + encoding.encode(init_str, EncoderTrap::Replace).map_err(|data| { + error::IrcError::CodecFailed { + codec: encoding.name(), + data: data.into_owned(), + } + }) }; let framed = MockStream::new(&initial?).framed(IrcCodec::new(config.encoding())?); @@ -139,17 +136,14 @@ impl Connection { info!("Added {} to trusted certificates.", cert_path); } let connector = builder.build()?; - let stream = Box::new(TcpStream::connect(&config.socket_addr()?, handle) - .map_err(|e| { - let res: error::Error = e.into(); - res - }) - .and_then(move |socket| { - connector.connect_async(&domain, socket).map_err( - |e| e.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( + |e| e.into(), + ) + })); Ok(ConnectionFuture::Secured(config, stream)) } else { info!("Connecting to {}.", config.server()?); @@ -172,7 +166,7 @@ impl Connection { impl Stream for Connection { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll, Self::Error> { match *self { @@ -185,7 +179,7 @@ impl Stream for Connection { impl Sink for Connection { type SinkItem = Message; - type SinkError = error::Error; + type SinkError = error::IrcError; fn start_send(&mut self, item: Self::SinkItem) -> StartSend { match *self { diff --git a/src/client/data/config.rs b/src/client/data/config.rs index 2d34c3f..fe2767d 100644 --- a/src/client/data/config.rs +++ b/src/client/data/config.rs @@ -3,9 +3,8 @@ use std::borrow::ToOwned; use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; -use std::io::{Error, ErrorKind}; use std::net::{SocketAddr, ToSocketAddrs}; -use std::path::Path; +use std::path::{Path, PathBuf}; #[cfg(feature = "json")] use serde_json; @@ -14,8 +13,10 @@ use serde_yaml; #[cfg(feature = "toml")] use toml; -use error; -use error::{Result, ResultExt}; +#[cfg(feature = "toml")] +use error::TomlError; +use error::{ConfigError, Result}; +use error::IrcError::InvalidConfig; /// Configuration data. #[derive(Clone, Deserialize, Serialize, Default, PartialEq, Debug)] @@ -88,9 +89,25 @@ pub struct Config { pub channel_keys: Option>, /// A map of additional options to be stored in config. pub options: Option>, + + /// The path that this configuration was loaded from. + /// + /// This should not be specified in any configuration. It will automatically be handled by the library. + pub path: Option, } impl Config { + fn with_path>(mut self, path: P) -> Config { + self.path = Some(path.as_ref().to_owned()); + self + } + + fn path(&self) -> String { + self.path.as_ref().map(|buf| buf.to_string_lossy().into_owned()).unwrap_or_else(|| { + "".to_owned() + }) + } + /// Loads a configuration from the desired path. This will use the file extension to detect /// which format to parse the file as (json, toml, or yaml). Using each format requires having /// its respective crate feature enabled. Only json is available by default. @@ -99,155 +116,171 @@ impl Config { let mut data = String::new(); file.read_to_string(&mut data)?; - match path.as_ref().extension().and_then(|s| s.to_str()) { - Some("json") => Config::load_json(&data), - Some("toml") => Config::load_toml(&data), - Some("yaml") | Some("yml") => Config::load_yaml(&data), - Some(ext) => Err(Error::new( - ErrorKind::InvalidInput, - format!("Failed to decode configuration of unknown format {}", ext), - ).into()), - None => Err(Error::new( - ErrorKind::InvalidInput, - "Failed to decode configuration of missing or non-unicode format.", - ).into()), - } + let res = match path.as_ref().extension().and_then(|s| s.to_str()) { + Some("json") => Config::load_json(&path, &data), + Some("toml") => Config::load_toml(&path, &data), + Some("yaml") | Some("yml") => Config::load_yaml(&path, &data), + Some(ext) => Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::UnknownConfigFormat { + format: ext.to_owned(), + }, + }), + None => Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::MissingExtension, + }), + }; + + res.map(|config| { + config.with_path(path) + }) } #[cfg(feature = "json")] - fn load_json(data: &str) -> Result { - serde_json::from_str(&data[..]).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to decode JSON configuration file.", - ).into(); - e + fn load_json>(path: &P, data: &str) -> Result { + serde_json::from_str(&data[..]).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidJson(e), + } }) } #[cfg(not(feature = "json"))] - fn load_json(_: &str) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "JSON file decoding is disabled.", - ).into()) + fn load_json>(path: &P, _: &str) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "JSON" + } + }) } #[cfg(feature = "toml")] - fn load_toml(data: &str) -> Result { - toml::from_str(&data[..]).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to decode TOML configuration file.", - ).into(); - e + fn load_toml>(path: &P, data: &str) -> Result { + toml::from_str(&data[..]).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidToml(TomlError::Read(e)), + } }) } #[cfg(not(feature = "toml"))] - fn load_toml(_: &str) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "TOML file decoding is disabled.", - ).into()) + fn load_toml>(path: &P, _: &str) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "TOML" + } + }) } #[cfg(feature = "yaml")] - fn load_yaml(data: &str) -> Result { - serde_yaml::from_str(&data[..]).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to decode YAML configuration file.", - ).into(); - e + fn load_yaml>(path: &P, data: &str) -> Result { + serde_yaml::from_str(&data[..]).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidYaml(e), + } }) } #[cfg(not(feature = "yaml"))] - fn load_yaml(_: &str) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "YAML file decoding is disabled.", - ).into()) + fn load_yaml>(path: &P, _: &str) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "YAML" + } + }) } /// Saves a configuration to the desired path. This will use the file extension to detect /// which format to parse the file as (json, toml, or yaml). Using each format requires having /// its respective crate feature enabled. Only json is available by default. - pub fn save>(&self, path: P) -> Result<()> { + pub fn save>(&mut self, path: P) -> Result<()> { + let _ = self.path.take(); let mut file = File::create(&path)?; let data = match path.as_ref().extension().and_then(|s| s.to_str()) { - Some("json") => self.save_json()?, - Some("toml") => self.save_toml()?, - Some("yaml") | Some("yml") => self.save_yaml()?, - Some(ext) => return Err(Error::new( - ErrorKind::InvalidInput, - format!("Failed to encode configuration of unknown format {}", ext), - ).into()), - None => return Err(Error::new( - ErrorKind::InvalidInput, - "Failed to encode configuration of missing or non-unicode format.", - ).into()), + Some("json") => self.save_json(&path)?, + Some("toml") => self.save_toml(&path)?, + Some("yaml") | Some("yml") => self.save_yaml(&path)?, + Some(ext) => return Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::UnknownConfigFormat { + format: ext.to_owned(), + }, + }), + None => return Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::MissingExtension, + }), }; file.write_all(data.as_bytes())?; + self.path = Some(path.as_ref().to_owned()); Ok(()) } #[cfg(feature = "json")] - fn save_json(&self) -> Result { - serde_json::to_string(self).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to encode JSON configuration file.", - ).into(); - e + fn save_json>(&self, path: &P) -> Result { + serde_json::to_string(self).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidJson(e), + } }) } #[cfg(not(feature = "json"))] - fn save_json(&self) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "JSON file encoding is disabled.", - ).into()) + fn save_json>(&self, path: &P) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "JSON" + } + }) } #[cfg(feature = "toml")] - fn save_toml(&self) -> Result { - toml::to_string(self).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to encode TOML configuration file.", - ).into(); - e + fn save_toml>(&self, path: &P) -> Result { + toml::to_string(self).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidToml(TomlError::Write(e)), + } }) } #[cfg(not(feature = "toml"))] - fn save_toml(&self) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "TOML file encoding is disabled.", - ).into()) + fn save_toml>(&self, path: &P) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "TOML" + } + }) } #[cfg(feature = "yaml")] - fn save_yaml(&self) -> Result { - serde_yaml::to_string(self).chain_err(|| { - let e: error::Error = Error::new( - ErrorKind::InvalidInput, - "Failed to encode YAML configuration file.", - ).into(); - e + fn save_yaml>(&self, path: &P) -> Result { + serde_yaml::to_string(self).map_err(|e| { + InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::InvalidYaml(e), + } }) } #[cfg(not(feature = "yaml"))] - fn save_yaml(&self) -> Result { - Err(Error::new( - ErrorKind::InvalidInput, - "YAML file encoding is disabled.", - ).into()) + fn save_yaml>(&self, path: &P) -> Result { + Err(InvalidConfig { + path: path.as_ref().to_string_lossy().into_owned(), + cause: ConfigError::ConfigFormatDisabled { + format: "YAML" + } + }) } /// Determines whether or not the nickname provided is the owner of the bot. @@ -260,7 +293,12 @@ impl Config { /// Gets the nickname specified in the configuration. pub fn nickname(&self) -> Result<&str> { - self.nickname.as_ref().map(|s| &s[..]).ok_or(error::ErrorKind::NicknameNotSpecified.into()) + self.nickname.as_ref().map(|s| &s[..]).ok_or_else(|| { + InvalidConfig { + path: self.path(), + cause: ConfigError::NicknameNotSpecified, + } + }) } /// Gets the bot's nickserv password specified in the configuration. @@ -292,8 +330,12 @@ impl Config { /// Gets the address of the server specified in the configuration. pub fn server(&self) -> Result<&str> { - self.server.as_ref().map(|s| &s[..]).ok_or(error::ErrorKind::NicknameNotSpecified.into()) - + self.server.as_ref().map(|s| &s[..]).ok_or_else(|| { + InvalidConfig { + path: self.path(), + cause: ConfigError::ServerNotSpecified, + } + }) } /// Gets the port of the server specified in the configuration. @@ -487,31 +529,27 @@ mod test { options: Some(HashMap::new()), use_mock_connection: None, mock_initial_value: None, + + ..Default::default() } } #[test] #[cfg(feature = "json")] - fn load() { - assert_eq!(Config::load(Path::new("client_config.json")).unwrap(), test_config()); - } - - #[test] - #[cfg(feature = "json")] - fn load_from_str() { - assert_eq!(Config::load("client_config.json").unwrap(), test_config()); + fn load_from_json() { + assert_eq!(Config::load("client_config.json").unwrap(), test_config().with_path("client_config.json")); } #[test] #[cfg(feature = "toml")] fn load_from_toml() { - assert_eq!(Config::load("client_config.toml").unwrap(), test_config()); + assert_eq!(Config::load("client_config.toml").unwrap(), test_config().with_path("client_config.toml")); } #[test] #[cfg(feature = "yaml")] fn load_from_yaml() { - assert_eq!(Config::load("client_config.yaml").unwrap(), test_config()); + assert_eq!(Config::load("client_config.yaml").unwrap(), test_config().with_path("client_config.yaml")); } #[test] diff --git a/src/client/reactor.rs b/src/client/reactor.rs index dca3fe9..5717164 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -41,7 +41,7 @@ use proto::Message; /// For a full example usage, see [irc::client::reactor](./index.html). pub struct IrcReactor { inner: Core, - handlers: Vec>>, + handlers: Vec>>, } impl IrcReactor { @@ -139,7 +139,7 @@ impl IrcReactor { pub fn register_server_with_handler( &mut self, server: IrcServer, handler: F ) where F: Fn(&IrcServer, Message) -> U + 'static, - U: IntoFuture + 'static { + U: IntoFuture + 'static { self.handlers.push(Box::new(server.stream().for_each(move |message| { handler(&server, message) }))); @@ -151,7 +151,7 @@ impl IrcReactor { /// be sufficient for most use cases. pub fn register_future( &mut self, future: F - ) where F: IntoFuture + 'static { + ) where F: IntoFuture + 'static { self.handlers.push(Box::new(future.into_future())) } diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 5d09319..3c92e59 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -97,7 +97,7 @@ pub mod utils; /// }).unwrap(); /// # } /// ``` -pub trait EachIncomingExt: Stream { +pub trait EachIncomingExt: Stream { /// Blocks on the stream, running the given function on each incoming message as they arrive. fn for_each_incoming(self, mut f: F) -> error::Result<()> where @@ -111,7 +111,7 @@ pub trait EachIncomingExt: Stream { } } -impl EachIncomingExt for T where T: Stream {} +impl EachIncomingExt for T where T: Stream {} /// An interface for communicating with an IRC server. pub trait Server { @@ -211,7 +211,7 @@ pub struct ServerStream { impl Stream for ServerStream { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll, Self::Error> { match try_ready!(self.stream.poll()) { @@ -352,7 +352,6 @@ impl ServerState { trace!("[RECV] {}", msg.to_string()); match msg.command { JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan), - /// This will panic if not specified. PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan), QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")), NICK(ref new_nick) => { @@ -442,7 +441,16 @@ impl ServerState { if self.config().umodes().is_empty() { Ok(()) } else { - self.send_mode(self.current_nickname(), &Mode::as_user_modes(self.config().umodes())?) + self.send_mode( + self.current_nickname(), &Mode::as_user_modes(self.config().umodes()).map_err(|e| { + error::IrcError::InvalidMessage { + string: format!( + "MODE {} {}", self.current_nickname(), self.config().umodes() + ), + cause: e, + } + })? + ) } } @@ -721,9 +729,8 @@ impl IrcServer { tx_view.send(conn.log_view()).unwrap(); let (sink, stream) = conn.split(); - let outgoing_future = sink.send_all(rx_outgoing.map_err(|_| { - let res: error::Error = error::ErrorKind::ChannelError.into(); - res + let outgoing_future = sink.send_all(rx_outgoing.map_err::(|_| { + unreachable!("futures::sync::mpsc::Receiver should never return Err"); })).map(|_| ()).map_err(|e| panic!("{}", e)); // Send the stream half back to the original thread. @@ -821,7 +828,7 @@ pub struct IrcServerFuture<'a> { impl<'a> Future for IrcServerFuture<'a> { type Item = PackedIrcServer; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll { let conn = try_ready!(self.conn.poll()); @@ -829,10 +836,11 @@ impl<'a> Future for IrcServerFuture<'a> { let view = conn.log_view(); let (sink, stream) = conn.split(); - let outgoing_future = sink.send_all(self.rx_outgoing.take().unwrap().map_err(|()| { - let res: error::Error = error::ErrorKind::ChannelError.into(); - res - })).map(|_| ()); + let outgoing_future = sink.send_all( + self.rx_outgoing.take().unwrap().map_err::(|()| { + unreachable!("futures::sync::mpsc::Receiver should never return Err"); + }) + ).map(|_| ()); let server = IrcServer { state: Arc::new(ServerState::new( @@ -850,7 +858,7 @@ impl<'a> Future for IrcServerFuture<'a> { /// This type should only be used by advanced users who are familiar with the implementation of this /// crate. An easy to use abstraction that does not require this knowledge is available via /// [IrcReactors](../reactor/struct.IrcReactor.html). -pub struct PackedIrcServer(pub IrcServer, pub Box>); +pub struct PackedIrcServer(pub IrcServer, pub Box>); #[cfg(test)] mod test { diff --git a/src/client/transport.rs b/src/client/transport.rs index ca52c91..dde82b2 100644 --- a/src/client/transport.rs +++ b/src/client/transport.rs @@ -88,12 +88,12 @@ where T: AsyncRead + AsyncWrite, { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll, Self::Error> { if self.ping_timed_out() { self.close()?; - return Err(error::ErrorKind::PingTimeout.into()) + return Err(error::IrcError::PingTimeout) } let timer_poll = self.ping_timer.poll()?; @@ -144,12 +144,12 @@ where T: AsyncRead + AsyncWrite, { type SinkItem = Message; - type SinkError = error::Error; + type SinkError = error::IrcError; fn start_send(&mut self, item: Self::SinkItem) -> StartSend { if self.ping_timed_out() { self.close()?; - Err(error::ErrorKind::PingTimeout.into()) + Err(error::IrcError::PingTimeout) } else { // Check if the oldest message in the rolling window is discounted. if let Async::Ready(()) = self.rolling_burst_window_front()? { @@ -180,7 +180,7 @@ where fn poll_complete(&mut self) -> Poll<(), Self::SinkError> { if self.ping_timed_out() { self.close()?; - Err(error::ErrorKind::PingTimeout.into()) + Err(error::IrcError::PingTimeout) } else { Ok(self.inner.poll_complete()?) } @@ -201,16 +201,12 @@ pub struct LogView { impl LogView { /// Gets a read guard for all the messages sent on the transport. pub fn sent(&self) -> error::Result>> { - self.sent.read().map_err( - |_| error::ErrorKind::PoisonedLog.into(), - ) + self.sent.read().map_err(|_| error::IrcError::PoisonedLog) } /// Gets a read guard for all the messages received on the transport. pub fn received(&self) -> error::Result>> { - self.received.read().map_err( - |_| error::ErrorKind::PoisonedLog.into(), - ) + self.received.read().map_err(|_| error::IrcError::PoisonedLog) } } @@ -250,13 +246,13 @@ where T: AsyncRead + AsyncWrite, { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn poll(&mut self) -> Poll, Self::Error> { match try_ready!(self.inner.poll()) { Some(msg) => { let recv: error::Result<_> = self.view.received.write().map_err(|_| { - error::ErrorKind::PoisonedLog.into() + error::IrcError::PoisonedLog }); recv?.push(msg.clone()); Ok(Async::Ready(Some(msg))) @@ -271,12 +267,12 @@ where T: AsyncRead + AsyncWrite, { type SinkItem = Message; - type SinkError = error::Error; + type SinkError = error::IrcError; fn start_send(&mut self, item: Self::SinkItem) -> StartSend { let res = self.inner.start_send(item.clone())?; let sent: error::Result<_> = self.view.sent.write().map_err(|_| { - error::ErrorKind::PoisonedLog.into() + error::IrcError::PoisonedLog }); sent?.push(item); Ok(res) diff --git a/src/error.rs b/src/error.rs index e4fea6b..47c0f1a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,67 +1,233 @@ -//! Errors for `irc` crate using `error_chain`. -#![allow(missing_docs)] +//! Errors for `irc` crate using `failure`. -error_chain! { - foreign_links { - Io(::std::io::Error); - Tls(::native_tls::Error); - Recv(::std::sync::mpsc::RecvError); - SendMessage(::futures::sync::mpsc::SendError<::proto::Message>); - OneShotCancelled(::futures::sync::oneshot::Canceled); - Timer(::tokio_timer::TimerError); - } +use std::io::Error as IoError; +use std::sync::mpsc::RecvError; - errors { - /// A parsing error for empty strings as messages. - ParseEmpty { - description("Cannot parse an empty string as a message.") - display("Cannot parse an empty string as a message.") - } +use futures::sync::mpsc::SendError; +use futures::sync::oneshot::Canceled; +use native_tls::Error as TlsError; +#[cfg(feature = "json")] +use serde_json::Error as JsonError; +#[cfg(feature = "yaml")] +use serde_yaml::Error as YamlError; +use tokio_timer::TimerError; +#[cfg(feature = "toml")] +use toml::de::Error as TomlReadError; +#[cfg(feature = "toml")] +use toml::ser::Error as TomlWriteError; - /// A parsing error for invalid or missing commands in messages. - InvalidCommand { - description("Message contained a missing or invalid Command.") - display("Message contained a missing or invalid Command.") - } +use proto::Message; - /// A parsing error for failures in subcommand parsing (e.g. CAP and metadata). - SubCommandParsingFailed { - description("Failed to parse an IRC subcommand.") - display("Failed to parse an IRC subcommand.") - } +/// A specialized `Result` type for the `irc` crate. +pub type Result = ::std::result::Result; - /// Failed to parse a mode correctly. - ModeParsingFailed { - description("Failed to parse a mode correctly.") - display("Failed to parse a mode correctly.") - } +/// The main crate-wide error type. +#[derive(Debug, Fail)] +pub enum IrcError { + /// An internal I/O error. + #[fail(display = "an io error occurred")] + Io(#[cause] IoError), - /// An error occurred on one of the internal channels of the `IrcServer`. - ChannelError { - description("An error occured on one of the IrcServer's internal channels.") - display("An error occured on one of the IrcServer's internal channels.") - } + /// An internal TLS error. + #[fail(display = "a TLS error occurred")] + Tls(#[cause] TlsError), - /// An error occured causing a mutex for a logged transport to be poisoned. - PoisonedLog { - description("An error occured causing a mutex for a logged transport to be poisoned.") - display("An error occured causing a mutex for a logged transport to be poisoned.") - } + /// An internal synchronous channel closed. + #[fail(display = "a sync channel closed")] + SyncChannelClosed(#[cause] RecvError), - /// Connection timed out due to no ping response. - PingTimeout { - description("The connection timed out due to no ping response.") - display("The connection timed out due to no ping response.") - } + /// An internal asynchronous channel closed. + #[fail(display = "an async channel closed")] + AsyncChannelClosed(#[cause] SendError), - NicknameNotSpecified { - description("No nickname was specified for use with this IrcServer.") - display("No nickname was specified for use with this IrcServer.") - } + /// An internal oneshot channel closed. + #[fail(display = "a oneshot channel closed")] + OneShotCanceled(#[cause] Canceled), - ServerNotSpecified { - description("No server was specified to connect to.") - display("No server was specified to connect to.") - } + /// An internal timer error. + #[fail(display = "timer failed")] + Timer(#[cause] TimerError), + + /// Error for invalid configurations. + #[fail(display = "invalid config: {}", path)] + InvalidConfig { + /// The path to the configuration, or "" if none specified. + path: String, + /// The detailed configuration error. + #[cause] + cause: ConfigError, + }, + + /// Error for invalid messages. + #[fail(display = "invalid message: {}", string)] + InvalidMessage { + /// The string that failed to parse. + string: String, + /// The detailed message parsing error. + #[cause] + cause: MessageParseError, + }, + + /// Mutex for a logged transport was poisoned making the log inaccessible. + #[fail(display = "mutex for a logged transport was poisoned")] + PoisonedLog, + + /// Ping timed out due to no response. + #[fail(display = "connection reset: no ping response")] + PingTimeout, + + /// Failed to lookup an unknown codec. + #[fail(display = "unknown codec: {}", codec)] + UnknownCodec { + /// The attempted codec. + codec: String, + }, + + /// Failed to encode or decode something with the given codec. + #[fail(display = "codec {} failed: {}", codec, data)] + CodecFailed { + /// The canonical codec name. + codec: &'static str, + /// The data that failed to encode or decode. + data: String, + }, +} + +/// Errors that occur when parsing messages. +#[derive(Debug, Fail)] +pub enum MessageParseError { + /// The message was empty. + #[fail(display = "empty message")] + EmptyMessage, + + /// The command was invalid (i.e. missing). + #[fail(display = "invalid command")] + InvalidCommand, + + /// The mode string was malformed. + #[fail(display = "invalid mode string: {}", string)] + InvalidModeString { + /// The invalid mode string. + string: String, + /// The detailed mode parsing error. + #[cause] + cause: ModeParseError, + }, + + /// The subcommand used was invalid. + #[fail(display = "invalid {} subcommand: {}", cmd, sub)] + InvalidSubcommand { + /// The command whose invalid subcommand was referenced. + cmd: &'static str, + /// The invalid subcommand. + sub: String, + } +} + +/// Errors that occur while parsing mode strings. +#[derive(Debug, Fail)] +pub enum ModeParseError { + /// Invalid modifier used in a mode string (only + and - are valid). + #[fail(display = "invalid mode modifier: {}", modifier)] + InvalidModeModifier { + /// The invalid mode modifier. + modifier: char, + }, + + /// Missing modifier used in a mode string. + #[fail(display = "missing mode modifier")] + MissingModeModifier, +} + +/// Errors that occur with configurations. +#[derive(Debug, Fail)] +pub enum ConfigError { + /// Failed to parse as TOML. + #[cfg(feature = "toml")] + #[fail(display = "invalid toml")] + InvalidToml(#[cause] TomlError), + + /// Failed to parse as JSON. + #[cfg(feature = "json")] + #[fail(display = "invalid json")] + InvalidJson(#[cause] JsonError), + + /// Failed to parse as YAML. + #[cfg(feature = "yaml")] + #[fail(display = "invalid yaml")] + InvalidYaml(#[cause] YamlError), + + /// Failed to parse the given format because it was disabled at compile-time. + #[fail(display = "config format disabled: {}", format)] + ConfigFormatDisabled { + /// The disabled file format. + format: &'static str, + }, + + /// Could not identify the given file format. + #[fail(display = "config format unknown: {}", format)] + UnknownConfigFormat { + /// The unknown file extension. + format: String, + }, + + /// File was missing an extension to identify file format. + #[fail(display = "missing format extension")] + MissingExtension, + + /// Configuration does not specify a nickname. + #[fail(display = "nickname not specified")] + NicknameNotSpecified, + + /// Configuration does not specify a server. + #[fail(display = "server not specified")] + ServerNotSpecified, +} + +/// A wrapper that combines toml's serialization and deserialization errors. +#[cfg(feature = "toml")] +#[derive(Debug, Fail)] +pub enum TomlError { + /// A TOML deserialization error. + #[fail(display = "deserialization failed")] + Read(#[cause] TomlReadError), + /// A TOML serialization error. + #[fail(display = "serialization failed")] + Write(#[cause] TomlWriteError), +} + +impl From for IrcError { + fn from(e: IoError) -> IrcError { + IrcError::Io(e) + } +} + +impl From for IrcError { + fn from(e: TlsError) -> IrcError { + IrcError::Tls(e) + } +} + +impl From for IrcError { + fn from(e: RecvError) -> IrcError { + IrcError::SyncChannelClosed(e) + } +} + +impl From> for IrcError { + fn from(e: SendError) -> IrcError { + IrcError::AsyncChannelClosed(e) + } +} + +impl From for IrcError { + fn from(e: Canceled) -> IrcError { + IrcError::OneShotCanceled(e) + } +} + +impl From for IrcError { + fn from(e: TimerError) -> IrcError { + IrcError::Timer(e) } } diff --git a/src/lib.rs b/src/lib.rs index 0fbff4c..b384c6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,13 +40,12 @@ //! ``` #![warn(missing_docs)] -#![recursion_limit="128"] extern crate bufstream; extern crate bytes; extern crate chrono; #[macro_use] -extern crate error_chain; +extern crate failure; extern crate encoding; #[macro_use] extern crate futures; diff --git a/src/proto/command.rs b/src/proto/command.rs index 3737f3c..7c0b70a 100644 --- a/src/proto/command.rs +++ b/src/proto/command.rs @@ -2,7 +2,7 @@ use std::ascii::AsciiExt; use std::str::FromStr; -use error; +use error::MessageParseError; use proto::{ChannelExt, ChannelMode, Mode, Response, UserMode}; /// List of all client commands as defined in [RFC 2812](http://tools.ietf.org/html/rfc2812). This @@ -446,7 +446,7 @@ impl<'a> From<&'a Command> for String { impl Command { /// Constructs a new Command. - pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> error::Result { + pub fn new(cmd: &str, args: Vec<&str>, suffix: Option<&str>) -> Result { Ok(if cmd.eq_ignore_ascii_case("PASS") { match suffix { Some(suffix) => { @@ -1653,8 +1653,9 @@ impl CapSubCommand { } impl FromStr for CapSubCommand { - type Err = error::Error; - fn from_str(s: &str) -> error::Result { + type Err = MessageParseError; + + fn from_str(s: &str) -> Result { if s.eq_ignore_ascii_case("LS") { Ok(CapSubCommand::LS) } else if s.eq_ignore_ascii_case("LIST") { @@ -1672,7 +1673,10 @@ impl FromStr for CapSubCommand { } else if s.eq_ignore_ascii_case("DEL") { Ok(CapSubCommand::DEL) } else { - Err(error::ErrorKind::SubCommandParsingFailed.into()) + Err(MessageParseError::InvalidSubcommand { + cmd: "CAP", + sub: s.to_owned(), + }) } } } @@ -1704,8 +1708,9 @@ impl MetadataSubCommand { } impl FromStr for MetadataSubCommand { - type Err = error::Error; - fn from_str(s: &str) -> error::Result { + type Err = MessageParseError; + + fn from_str(s: &str) -> Result { if s.eq_ignore_ascii_case("GET") { Ok(MetadataSubCommand::GET) } else if s.eq_ignore_ascii_case("LIST") { @@ -1715,7 +1720,10 @@ impl FromStr for MetadataSubCommand { } else if s.eq_ignore_ascii_case("CLEAR") { Ok(MetadataSubCommand::CLEAR) } else { - Err(error::ErrorKind::SubCommandParsingFailed.into()) + Err(MessageParseError::InvalidSubcommand { + cmd: "METADATA", + sub: s.to_owned(), + }) } } } @@ -1743,8 +1751,9 @@ impl BatchSubCommand { } impl FromStr for BatchSubCommand { - type Err = error::Error; - fn from_str(s: &str) -> error::Result { + type Err = MessageParseError; + + fn from_str(s: &str) -> Result { if s.eq_ignore_ascii_case("NETSPLIT") { Ok(BatchSubCommand::NETSPLIT) } else if s.eq_ignore_ascii_case("NETJOIN") { diff --git a/src/proto/irc.rs b/src/proto/irc.rs index b01270f..e0b2fca 100644 --- a/src/proto/irc.rs +++ b/src/proto/irc.rs @@ -20,7 +20,7 @@ impl IrcCodec { impl Decoder for IrcCodec { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn decode(&mut self, src: &mut BytesMut) -> error::Result> { self.inner.decode(src).and_then(|res| { @@ -31,7 +31,7 @@ impl Decoder for IrcCodec { impl Encoder for IrcCodec { type Item = Message; - type Error = error::Error; + type Error = error::IrcError; fn encode(&mut self, msg: Message, dst: &mut BytesMut) -> error::Result<()> { diff --git a/src/proto/line.rs b/src/proto/line.rs index ce59c58..2e1ade2 100644 --- a/src/proto/line.rs +++ b/src/proto/line.rs @@ -28,7 +28,7 @@ impl LineCodec { impl Decoder for LineCodec { type Item = String; - type Error = error::Error; + 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') { @@ -53,7 +53,7 @@ impl Decoder for LineCodec { impl Encoder for LineCodec { type Item = String; - type Error = error::Error; + type Error = error::IrcError; fn encode(&mut self, msg: String, dst: &mut BytesMut) -> error::Result<()> { // Encode the message using the codec's encoding. diff --git a/src/proto/message.rs b/src/proto/message.rs index 8605cc2..1c2a3af 100644 --- a/src/proto/message.rs +++ b/src/proto/message.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use std::str::FromStr; use error; -use error::{Error, ErrorKind}; +use error::{IrcError, MessageParseError}; use proto::{Command, ChannelExt}; /// A data structure representing an IRC message according to the protocol specification. It @@ -43,7 +43,7 @@ impl Message { command: &str, args: Vec<&str>, suffix: Option<&str>, - ) -> error::Result { + ) -> Result { Message::with_tags(None, prefix, command, args, suffix) } @@ -56,7 +56,7 @@ impl Message { command: &str, args: Vec<&str>, suffix: Option<&str>, - ) -> error::Result { + ) -> Result { Ok(Message { tags: tags, prefix: prefix.map(|s| s.to_owned()), @@ -170,13 +170,18 @@ impl From for Message { } impl FromStr for Message { - type Err = Error; + type Err = IrcError; fn from_str(s: &str) -> Result { - let mut state = s; if s.is_empty() { - return Err(ErrorKind::ParseEmpty.into()); + return Err(IrcError::InvalidMessage { + string: s.to_owned(), + cause: MessageParseError::EmptyMessage, + }) } + + let mut state = s; + let tags = if state.starts_with('@') { let tags = state.find(' ').map(|i| &state[1..i]); state = state.find(' ').map_or("", |i| &state[i + 1..]); @@ -193,6 +198,7 @@ impl FromStr for Message { } else { None }; + let prefix = if state.starts_with(':') { let prefix = state.find(' ').map(|i| &state[1..i]); state = state.find(' ').map_or("", |i| &state[i + 1..]); @@ -200,6 +206,7 @@ impl FromStr for Message { } else { None }; + let line_ending_len = if state.ends_with("\r\n") { "\r\n" } else if state.ends_with('\r') { @@ -209,6 +216,7 @@ impl FromStr for Message { } else { "" }.len(); + let suffix = if state.contains(" :") { let suffix = state.find(" :").map(|i| &state[i + 2..state.len() - line_ending_len]); state = state.find(" :").map_or("", |i| &state[..i + 1]); @@ -217,24 +225,33 @@ impl FromStr for Message { state = &state[..state.len() - line_ending_len]; None }; + let command = match state.find(' ').map(|i| &state[..i]) { Some(cmd) => { state = state.find(' ').map_or("", |i| &state[i + 1..]); cmd } // If there's no arguments but the "command" starts with colon, it's not a command. - None if state.starts_with(":") => return Err(ErrorKind::InvalidCommand.into()), + None if state.starts_with(":") => return Err(IrcError::InvalidMessage { + string: s.to_owned(), + cause: MessageParseError::InvalidCommand, + }), // If there's no arguments following the command, the rest of the state is the command. None => { let cmd = state; state = ""; cmd }, - }; + let args: Vec<_> = state.splitn(14, ' ').filter(|s| !s.is_empty()).collect(); - Message::with_tags(tags, prefix, command, args, suffix) - .map_err(|_| ErrorKind::InvalidCommand.into()) + + Message::with_tags(tags, prefix, command, args, suffix).map_err(|e| { + IrcError::InvalidMessage { + string: s.to_owned(), + cause: e, + } + }) } } diff --git a/src/proto/mode.rs b/src/proto/mode.rs index ecf8bce..806104f 100644 --- a/src/proto/mode.rs +++ b/src/proto/mode.rs @@ -1,7 +1,9 @@ //! A module defining an API for IRC user and channel modes. use std::fmt; -use error; +use error::MessageParseError; +use error::MessageParseError::InvalidModeString; +use error::ModeParseError::*; use proto::Command; /// A marker trait for different kinds of Modes. @@ -48,10 +50,10 @@ impl ModeType for UserMode { } impl UserMode { - fn from_char(c: char) -> error::Result { + fn from_char(c: char) -> UserMode { use self::UserMode::*; - Ok(match c { + match c { 'a' => Away, 'i' => Invisible, 'w' => Wallops, @@ -61,7 +63,7 @@ impl UserMode { 's' => ServerNotices, 'x' => MaskedHost, _ => Unknown(c), - }) + } } } @@ -141,10 +143,10 @@ impl ModeType for ChannelMode { } impl ChannelMode { - fn from_char(c: char) -> error::Result { + fn from_char(c: char) -> ChannelMode { use self::ChannelMode::*; - Ok(match c { + match c { 'b' => Ban, 'e' => Exception, 'l' => Limit, @@ -162,7 +164,7 @@ impl ChannelMode { 'h' => Halfop, 'v' => Voice, _ => Unknown(c), - }) + } } } @@ -242,7 +244,7 @@ enum PlusMinus { impl Mode { // TODO: turning more edge cases into errors. /// Parses the specified mode string as user modes. - pub fn as_user_modes(s: &str) -> error::Result>> { + pub fn as_user_modes(s: &str) -> Result>, MessageParseError> { use self::PlusMinus::*; let mut res = vec![]; @@ -255,11 +257,18 @@ impl Mode { let init = match chars.next() { Some('+') => Plus, Some('-') => Minus, - _ => return Err(error::ErrorKind::ModeParsingFailed.into()), + Some(c) => return Err(InvalidModeString { + string: s.to_owned(), + cause: InvalidModeModifier { modifier: c }, + }), + None => return Err(InvalidModeString { + string: s.to_owned(), + cause: MissingModeModifier, + }), }; for c in chars { - let mode = UserMode::from_char(c)?; + let mode = UserMode::from_char(c); let arg = if mode.takes_arg() { pieces.next() } else { @@ -281,7 +290,7 @@ impl Mode { impl Mode { // TODO: turning more edge cases into errors. /// Parses the specified mode string as channel modes. - pub fn as_channel_modes(s: &str) -> error::Result>> { + pub fn as_channel_modes(s: &str) -> Result>, MessageParseError> { use self::PlusMinus::*; let mut res = vec![]; @@ -294,11 +303,18 @@ impl Mode { let init = match chars.next() { Some('+') => Plus, Some('-') => Minus, - _ => return Err(error::ErrorKind::ModeParsingFailed.into()), + Some(c) => return Err(InvalidModeString { + string: s.to_owned(), + cause: InvalidModeModifier { modifier: c }, + }), + None => return Err(InvalidModeString { + string: s.to_owned(), + cause: MissingModeModifier, + }), }; for c in chars { - let mode = ChannelMode::from_char(c)?; + let mode = ChannelMode::from_char(c); let arg = if mode.takes_arg() { pieces.next() } else { From 90276b63b0bd3e865c659b2e8640298fc268a2f3 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 01:03:49 +0100 Subject: [PATCH 23/31] Removed multiserver examples, and renamed nothreads to multiserver. --- examples/multiserver.rs | 35 +++++++++++---------- examples/multiserver_complex.rs | 56 --------------------------------- examples/nothreads.rs | 51 ------------------------------ 3 files changed, 18 insertions(+), 124 deletions(-) delete mode 100644 examples/multiserver_complex.rs delete mode 100644 examples/nothreads.rs diff --git a/examples/multiserver.rs b/examples/multiserver.rs index fd8af4b..b513946 100644 --- a/examples/multiserver.rs +++ b/examples/multiserver.rs @@ -1,8 +1,6 @@ -extern crate futures; extern crate irc; use std::default::Default; -use futures::stream::MergedItem; use irc::error; use irc::client::prelude::*; @@ -13,27 +11,30 @@ fn main() { channels: Some(vec!["#irc-crate".to_owned()]), ..Default::default() }; + let cfg2 = Config { - nickname: Some("pickles".to_owned()), - server: Some("irc.pdgn.co".to_owned()), + nickname: Some("bananas".to_owned()), + server: Some("irc.fyrechat.net".to_owned()), channels: Some(vec!["#irc-crate".to_owned()]), - use_ssl: Some(true), ..Default::default() }; - let server1 = IrcServer::from_config(cfg1).unwrap(); - let server2 = IrcServer::from_config(cfg2).unwrap(); - server1.identify().unwrap(); - server2.identify().unwrap(); + let configs = vec![cfg1, cfg2]; - server1.stream().merge(server2.stream()).for_each(|pair| match pair { - MergedItem::First(message) => process_msg(&server1, message), - MergedItem::Second(message) => process_msg(&server2, message), - MergedItem::Both(msg1, msg2) => { - process_msg(&server1, msg1).unwrap(); - process_msg(&server2, msg2) - } - }).wait().unwrap() + let mut reactor = IrcReactor::new().unwrap(); + + for config in configs { + // Immediate errors like failure to resolve the server's name or to establish any connection will + // manifest here in the result of prepare_server_and_connect. + let server = reactor.prepare_server_and_connect(&config).unwrap(); + server.identify().unwrap(); + // Here, we tell the reactor to setup this server for future handling (in run) using the specified + // handler function process_msg. + reactor.register_server_with_handler(server, process_msg); + } + + // Runtime errors like a dropped connection will manifest here in the result of run. + reactor.run().unwrap(); } fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { diff --git a/examples/multiserver_complex.rs b/examples/multiserver_complex.rs deleted file mode 100644 index 30d0b64..0000000 --- a/examples/multiserver_complex.rs +++ /dev/null @@ -1,56 +0,0 @@ -extern crate futures; -extern crate irc; -extern crate tokio_core; - -use std::default::Default; -use futures::future; -use irc::error; -use irc::client::prelude::*; -use tokio_core::reactor::Core; - -fn main() { - let cfg1 = Config { - nickname: Some("pickles".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), - ..Default::default() - }; - let cfg2 = Config { - nickname: Some("pickles".to_owned()), - server: Some("irc.pdgn.co".to_owned()), - channels: Some(vec!["#irc-crate".to_owned()]), - use_ssl: Some(true), - ..Default::default() - }; - - let configs = vec![cfg1, cfg2]; - - // Create an event loop to run the multiple connections on. - let mut reactor = Core::new().unwrap(); - let handle = reactor.handle(); - - for config in configs { - let server = IrcServer::from_config(config).unwrap(); - server.identify().unwrap(); - - handle.spawn(server.stream().for_each(move |message| { - process_msg(&server, message) - }).map_err(|e| Err(e).unwrap())) - } - - // You might instead want to join all the futures and run them directly. - reactor.run(future::empty::<(), ()>()).unwrap(); -} - -fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { - print!("{}", message); - match message.command { - Command::PRIVMSG(ref target, ref msg) => { - if msg.contains("pickles") { - server.send_privmsg(target, "Hi!")?; - } - } - _ => (), - } - Ok(()) -} diff --git a/examples/nothreads.rs b/examples/nothreads.rs deleted file mode 100644 index b513946..0000000 --- a/examples/nothreads.rs +++ /dev/null @@ -1,51 +0,0 @@ -extern crate irc; - -use std::default::Default; -use irc::error; -use irc::client::prelude::*; - -fn main() { - let cfg1 = Config { - nickname: Some("pickles".to_owned()), - server: Some("irc.fyrechat.net".to_owned()), - channels: Some(vec!["#irc-crate".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()]), - ..Default::default() - }; - - let configs = vec![cfg1, cfg2]; - - let mut reactor = IrcReactor::new().unwrap(); - - for config in configs { - // Immediate errors like failure to resolve the server's name or to establish any connection will - // manifest here in the result of prepare_server_and_connect. - let server = reactor.prepare_server_and_connect(&config).unwrap(); - server.identify().unwrap(); - // Here, we tell the reactor to setup this server for future handling (in run) using the specified - // handler function process_msg. - reactor.register_server_with_handler(server, process_msg); - } - - // Runtime errors like a dropped connection will manifest here in the result of run. - reactor.run().unwrap(); -} - -fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { - print!("{}", message); - match message.command { - Command::PRIVMSG(ref target, ref msg) => { - if msg.contains("pickles") { - server.send_privmsg(target, "Hi!")?; - } - } - _ => (), - } - Ok(()) -} From afe3558880e0d4c66be51552e3908a6184ade1a5 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 01:14:55 +0100 Subject: [PATCH 24/31] Cleaned up a bunch of code with clippy. --- src/client/conn.rs | 6 +++--- src/client/reactor.rs | 2 +- src/client/server/mod.rs | 27 ++++++++++++--------------- src/client/server/utils.rs | 8 ++++---- src/client/transport.rs | 8 ++++---- src/lib.rs | 6 +++--- src/proto/command.rs | 1 - src/proto/message.rs | 4 ++-- 8 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/client/conn.rs b/src/client/conn.rs index 8dbdc0e..dbe7f5e 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -80,19 +80,19 @@ impl<'a> Future for ConnectionFuture<'a> { fn poll(&mut self) -> Poll { match *self { - ConnectionFuture::Unsecured(ref config, ref mut inner) => { + ConnectionFuture::Unsecured(config, ref mut inner) => { let framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?); let transport = IrcTransport::new(config, framed); Ok(Async::Ready(Connection::Unsecured(transport))) } - ConnectionFuture::Secured(ref config, ref mut inner) => { + ConnectionFuture::Secured(config, ref mut inner) => { let framed = try_ready!(inner.poll()).framed(IrcCodec::new(config.encoding())?); let transport = IrcTransport::new(config, framed); Ok(Async::Ready(Connection::Secured(transport))) } - ConnectionFuture::Mock(ref config) => { + ConnectionFuture::Mock(config) => { let enc: error::Result<_> = encoding_from_whatwg_label( config.encoding() ).ok_or_else(|| io::Error::new( diff --git a/src/client/reactor.rs b/src/client/reactor.rs index dca3fe9..b6b995c 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -38,7 +38,7 @@ use proto::Message; /// all connected servers as the application runs. It can be used to run multiple servers on the /// same thread, as well as to get better control over error management in an IRC client. /// -/// For a full example usage, see [irc::client::reactor](./index.html). +/// For a full example usage, see [`irc::client::reactor`](./index.html). pub struct IrcReactor { inner: Core, handlers: Vec>>, diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 5d09319..eeb8f06 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -1,11 +1,11 @@ //! The primary API for communicating with an IRC server. //! //! This API provides the ability to connect to an IRC server via the -//! [IrcServer](struct.IrcServer.html) type. The [Server](trait.Server.html) trait that -//! [IrcServer](struct.IrcServer.html) implements provides methods for communicating with this -//! server. An extension trait, [ServerExt](./utils/trait.ServerExt.html), provides short-hand for +//! [`IrcServer`](struct.IrcServer.html) type. The [`Server`](trait.Server.html) trait that +//! [`IrcServer`](struct.IrcServer.html) implements provides methods for communicating with this +//! server. An extension trait, [`ServerExt`](./utils/trait.ServerExt.html), provides short-hand for //! sending a variety of important messages without referring to their entries in -//! [proto::command](../../proto/command/enum.Command.html). +//! [`proto::command`](../../proto/command/enum.Command.html). //! //! # Examples //! @@ -45,8 +45,6 @@ //! }).unwrap(); //! # } //! ``` -#[cfg(feature = "ctcp")] -use std::ascii::AsciiExt; use std::collections::HashMap; use std::path::Path; use std::sync::{Arc, Mutex, RwLock}; @@ -73,9 +71,9 @@ pub mod utils; /// Trait extending all IRC streams with `for_each_incoming` convenience function. /// -/// This is typically used in conjunction with [Server::stream](trait.Server.html#tymethod.stream) +/// This is typically used in conjunction with [`Server::stream`](trait.Server.html#tymethod.stream) /// in order to use an API akin to -/// [Server::for_each_incoming](trait.Server.html#method.for_each_incoming). +/// [`Server::for_each_incoming`](trait.Server.html#method.for_each_incoming). /// /// # Example /// @@ -352,7 +350,6 @@ impl ServerState { trace!("[RECV] {}", msg.to_string()); match msg.command { JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan), - /// This will panic if not specified. PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan), QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")), NICK(ref new_nick) => { @@ -595,12 +592,12 @@ impl ServerState { /// /// The type itself provides a number of methods to create new connections, but most of the API /// surface is in the form of the [Server](trait.Server.html) and -/// [ServerExt](./utils/trait.ServerExt.html) traits that provide methods of communicating with the -/// server after connection. Cloning an `IrcServer` is relatively cheap, as it's equivalent to +/// [`ServerExt`](./utils/trait.ServerExt.html) traits that provide methods of communicating with +/// the server after connection. Cloning an `IrcServer` is relatively cheap, as it's equivalent to /// cloning a single `Arc`. This may be useful for setting up multiple threads with access to one /// connection. /// -/// For a full example usage, see [irc::client::server](./index.html). +/// For a full example usage, see [`irc::client::server`](./index.html). #[derive(Clone, Debug)] pub struct IrcServer { /// The internal, thread-safe server state. @@ -748,7 +745,7 @@ impl IrcServer { /// [futures](http://docs.rs/futures). Additionally, you can find detailed tutorials on using /// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). An easy /// to use abstraction that does not require this knowledge is available via - /// [IrcReactors](../reactor/struct.IrcReactor.html). + /// [`IrcReactors`](../reactor/struct.IrcReactor.html). /// /// # Example /// ```no_run @@ -809,7 +806,7 @@ impl IrcServer { /// use cases. To learn more, you can view the documentation for the /// [futures](https://docs.rs/futures/) crate, or the tutorials for /// [tokio](https://tokio.rs/docs/getting-started/futures/). An easy to use abstraction that does -/// not require this knowledge is available via [IrcReactors](../reactor/struct.IrcReactor.html). +/// not require this knowledge is available via [`IrcReactors`](../reactor/struct.IrcReactor.html). #[derive(Debug)] pub struct IrcServerFuture<'a> { conn: ConnectionFuture<'a>, @@ -849,7 +846,7 @@ impl<'a> Future for IrcServerFuture<'a> { /// /// This type should only be used by advanced users who are familiar with the implementation of this /// crate. An easy to use abstraction that does not require this knowledge is available via -/// [IrcReactors](../reactor/struct.IrcReactor.html). +/// [`IrcReactors`](../reactor/struct.IrcReactor.html). pub struct PackedIrcServer(pub IrcServer, pub Box>); #[cfg(test)] diff --git a/src/client/server/utils.rs b/src/client/server/utils.rs index 8105c19..2ee3e4f 100644 --- a/src/client/server/utils.rs +++ b/src/client/server/utils.rs @@ -1,12 +1,12 @@ //! Utilities and shortcuts for working with IRC servers. //! -//! This module provides the [ServerExt](trait.ServerExt.html) trait which is the idiomatic way of +//! This module provides the [`ServerExt`](trait.ServerExt.html) trait which is the idiomatic way of //! sending messages to an IRC server. This trait is automatically implemented for everything that -//! implements [Server](../trait.Server.html) and is designed to provide important functionality +//! implements [`Server`](../trait.Server.html) and is designed to provide important functionality //! without clutter. //! //! # Examples -//! +//! //! Using these APIs, we can connect to a server and send a one-off message (in this case, //! identifying with the server). //! @@ -15,7 +15,7 @@ //! use irc::client::prelude::{IrcServer, ServerExt}; //! //! # fn main() { -//! let server = IrcServer::new("config.toml").unwrap(); +//! let server = IrcServer::new("config.toml").unwrap(); //! // identify and send_privmsg both come from `ServerExt` //! server.identify().unwrap(); //! server.send_privmsg("#example", "Hello, world!").unwrap(); diff --git a/src/client/transport.rs b/src/client/transport.rs index ca52c91..08fbd79 100644 --- a/src/client/transport.rs +++ b/src/client/transport.rs @@ -44,11 +44,11 @@ where inner: inner, burst_timer: tokio_timer::wheel().build(), rolling_burst_window: VecDeque::new(), - burst_window_length: config.burst_window_length() as u64, - max_burst_messages: config.max_messages_in_burst() as u64, + burst_window_length: u64::from(config.burst_window_length()), + max_burst_messages: u64::from(config.max_messages_in_burst()), current_burst_messages: 0, - ping_timer: timer.interval(Duration::from_secs(config.ping_time() as u64)), - ping_timeout: config.ping_timeout() as u64, + ping_timer: timer.interval(Duration::from_secs(u64::from(config.ping_time()))), + ping_timeout: u64::from(config.ping_timeout()), last_ping_data: String::new(), last_ping_sent: Instant::now(), last_pong_received: Instant::now(), diff --git a/src/lib.rs b/src/lib.rs index 0fbff4c..3b85507 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! A simple, thread-safe, and async-friendly library for IRC clients. //! //! # Quick Start -//! The main public API is entirely exported in [client::prelude](./client/prelude/index.html). This +//! The main public API is entirely exported in [`client::prelude`](./client/prelude/index.html). This //! should include everything necessary to write an IRC client or bot. //! //! # A Whirlwind Tour @@ -11,7 +11,7 @@ //! implementation that could in principle be used in either client or server software. Both modules //! feature a number of components that are low-level and can be used to build alternative APIs for //! the IRC protocol. For the average user, the higher-level components for an IRC client are all -//! re-exported in [client::prelude](./client/prelude/index.html). That module serves as the best +//! re-exported in [`client::prelude`](./client/prelude/index.html). That module serves as the best //! starting point for a new user trying to understand the high-level API. //! //! # Example @@ -72,7 +72,7 @@ pub mod client; pub mod error; pub mod proto; -const VERSION_STR: &'static str = concat!( +const VERSION_STR: &str = concat!( env!("CARGO_PKG_NAME"), ":", env!("CARGO_PKG_VERSION"), diff --git a/src/proto/command.rs b/src/proto/command.rs index 3737f3c..e11b9c8 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; diff --git a/src/proto/message.rs b/src/proto/message.rs index 8605cc2..a7c0338 100644 --- a/src/proto/message.rs +++ b/src/proto/message.rs @@ -85,7 +85,7 @@ impl Message { s.find('@'), s.find('.'), ) { - (Some(i), _, _) => Some(&s[..i]), // '!' [ '@' ] + (Some(i), _, _) | // '!' [ '@' ] (None, Some(i), _) => Some(&s[..i]), // '@' (None, None, None) => Some(s), // _ => None, // @@ -223,7 +223,7 @@ impl FromStr for Message { cmd } // If there's no arguments but the "command" starts with colon, it's not a command. - None if state.starts_with(":") => return Err(ErrorKind::InvalidCommand.into()), + None if state.starts_with(':') => return Err(ErrorKind::InvalidCommand.into()), // If there's no arguments following the command, the rest of the state is the command. None => { let cmd = state; From 7a1d1d2af6fb58a701f233dbbc03cbdc0011bb6f Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 01:50:39 +0100 Subject: [PATCH 25/31] Added a reactor version of the simple example. --- examples/reactor.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 examples/reactor.rs diff --git a/examples/reactor.rs b/examples/reactor.rs new file mode 100644 index 0000000..0fe963c --- /dev/null +++ b/examples/reactor.rs @@ -0,0 +1,34 @@ +extern crate irc; + +use std::default::Default; +use irc::client::prelude::*; + +// This example is meant to be a direct analogue to simple.rs using the reactor API. +fn main() { + let config = Config { + nickname: Some("pickles".to_owned()), + alt_nicks: Some(vec!["bananas".to_owned(), "apples".to_owned()]), + server: Some("irc.fyrechat.net".to_owned()), + channels: Some(vec!["#irc-crate".to_owned()]), + ..Default::default() + }; + + let reactor = IrcReactor::new().unwrap(); + let server = reactor.prepare_server_and_connect(&config).unwrap(); + server.identify().unwrap(); + + reactor.register_server_with_handler(server, |message| { + print!("{}", message); + match message.command { + Command::PRIVMSG(ref target, ref msg) => { + if msg.contains("pickles") { + server.send_privmsg(target, "Hi!").unwrap(); + } + } + _ => (), + } + Ok(()) + }); + + reactor.run().unwrap(); +} From d6786936fa01a5e8ee70da7c55850621c6bc5bad Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 01:59:47 +0100 Subject: [PATCH 26/31] Fixed the code that shouldn't have worked without AsciiExt. --- src/client/server/mod.rs | 2 ++ src/proto/command.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/client/server/mod.rs b/src/client/server/mod.rs index 2c109ea..e06f117 100644 --- a/src/client/server/mod.rs +++ b/src/client/server/mod.rs @@ -45,6 +45,8 @@ //! }).unwrap(); //! # } //! ``` +#[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 71fc757..7c0b70a 100644 --- a/src/proto/command.rs +++ b/src/proto/command.rs @@ -1,4 +1,5 @@ //! Enumeration of all available client commands. +use std::ascii::AsciiExt; use std::str::FromStr; use error::MessageParseError; From af72e57c5f87776da0021c4eb572451141c1682b Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 02:00:04 +0100 Subject: [PATCH 27/31] Cleaned up a bunch of the examples. --- examples/reactor.rs | 13 +++++-------- examples/reconnector.rs | 13 +++++-------- examples/repeater.rs | 25 +++++++++++-------------- examples/simple.rs | 11 ++++------- examples/simple_ssl.rs | 11 ++++------- 5 files changed, 29 insertions(+), 44 deletions(-) diff --git a/examples/reactor.rs b/examples/reactor.rs index 0fe963c..0a9f21f 100644 --- a/examples/reactor.rs +++ b/examples/reactor.rs @@ -13,19 +13,16 @@ fn main() { ..Default::default() }; - let reactor = IrcReactor::new().unwrap(); + let mut reactor = IrcReactor::new().unwrap(); let server = reactor.prepare_server_and_connect(&config).unwrap(); server.identify().unwrap(); - reactor.register_server_with_handler(server, |message| { + reactor.register_server_with_handler(server, |server, message| { print!("{}", message); - match message.command { - Command::PRIVMSG(ref target, ref msg) => { - if msg.contains("pickles") { - server.send_privmsg(target, "Hi!").unwrap(); - } + if let Command::PRIVMSG(ref target, ref msg) = message.command { + if msg.contains("pickles") { + server.send_privmsg(target, "Hi!")?; } - _ => (), } Ok(()) }); diff --git a/examples/reconnector.rs b/examples/reconnector.rs index 0a29be5..17b0d91 100644 --- a/examples/reconnector.rs +++ b/examples/reconnector.rs @@ -46,15 +46,12 @@ fn main() { fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { print!("{}", message); - match message.command { - Command::PRIVMSG(ref target, ref msg) => { - if msg.contains("pickles") { - server.send_privmsg(target, "Hi!")?; - } else if msg.contains("quit") { - server.send_quit("bye")?; - } + if let Command::PRIVMSG(ref target, ref msg) = message.command { + if msg.contains("pickles") { + server.send_privmsg(target, "Hi!")?; + } else if msg.contains("quit") { + server.send_quit("bye")?; } - _ => (), } Ok(()) } diff --git a/examples/repeater.rs b/examples/repeater.rs index ecc38d2..fa970ef 100644 --- a/examples/repeater.rs +++ b/examples/repeater.rs @@ -20,24 +20,21 @@ fn main() { server.for_each_incoming(|message| { print!("{}", message); - match message.command { - Command::PRIVMSG(ref target, ref msg) => { - if msg.starts_with(server.current_nickname()) { - let tokens: Vec<_> = msg.split(' ').collect(); - if tokens.len() > 2 { - let n = tokens[0].len() + tokens[1].len() + 2; - if let Ok(count) = tokens[1].parse::() { - for _ in 0..count { - server.send_privmsg( - message.response_target().unwrap_or(target), - &msg[n..] - ).unwrap(); - } + if let Command::PRIVMSG(ref target, ref msg) = message.command { + if msg.starts_with(server.current_nickname()) { + let tokens: Vec<_> = msg.split(' ').collect(); + if tokens.len() > 2 { + let n = tokens[0].len() + tokens[1].len() + 2; + if let Ok(count) = tokens[1].parse::() { + for _ in 0..count { + server.send_privmsg( + message.response_target().unwrap_or(target), + &msg[n..] + ).unwrap(); } } } } - _ => (), } }).unwrap() } diff --git a/examples/simple.rs b/examples/simple.rs index 69d2fd6..c063a4f 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -17,13 +17,10 @@ fn main() { server.for_each_incoming(|message| { print!("{}", message); - match message.command { - Command::PRIVMSG(ref target, ref msg) => { - if msg.contains("pickles") { - server.send_privmsg(target, "Hi!").unwrap(); - } + if let Command::PRIVMSG(ref target, ref msg) = message.command { + if msg.contains("pickles") { + server.send_privmsg(target, "Hi!").unwrap(); } - _ => (), } - }).unwrap() + }).unwrap(); } diff --git a/examples/simple_ssl.rs b/examples/simple_ssl.rs index 7c13c30..a3fe915 100644 --- a/examples/simple_ssl.rs +++ b/examples/simple_ssl.rs @@ -17,13 +17,10 @@ fn main() { server.for_each_incoming(|message| { print!("{}", message); - match message.command { - Command::PRIVMSG(ref target, ref msg) => { - if msg.contains("pickles") { - server.send_privmsg(target, "Hi!").unwrap(); - } + if let Command::PRIVMSG(ref target, ref msg) = message.command { + if msg.contains("pickles") { + server.send_privmsg(target, "Hi!").unwrap(); } - _ => (), } - }).unwrap() + }).unwrap(); } From 9c574b00c0bd5cf997a06c0a52703fbd0fc3121a Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 02:02:45 +0100 Subject: [PATCH 28/31] Moved prelude module into its own file. --- src/client/mod.rs | 34 +--------------------------------- src/client/prelude.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 33 deletions(-) create mode 100644 src/client/prelude.rs diff --git a/src/client/mod.rs b/src/client/mod.rs index b7287fd..f8b6e6e 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -2,40 +2,8 @@ pub mod conn; pub mod data; +pub mod prelude; pub mod reactor; pub mod server; pub mod transport; -pub mod prelude { - //! A client-side IRC prelude, re-exporting the complete high-level IRC client API. - //! - //! # Structure - //! A connection to an IRC server is represented by an `IrcServer` which is configured using a - //! `Config` struct that defines data such as which server to connect to, on what port, and - //! using what nickname. The `Server` trait provides an API for actually interacting with the - //! server once a connection has been established. This API intentionally offers only a single - //! method to send `Commands` because it makes it easy to see the whole set of possible - //! interactions with a server. The `ServerExt` trait addresses this deficiency by defining a - //! number of methods that provide a more clear and succinct interface for sending various - //! common IRC commands to the server. An `IrcReactor` can be used to create and manage multiple - //! `IrcServers` with more fine-grained control over error management. - //! - //! The various `proto` types capture details of the IRC protocol that are used throughout the - //! client API. `Message`, `Command`, and `Response` are used to send and receive messages along - //! the connection, and are naturally at the heart of communication in the IRC protocol. - //! `Capability` and `NegotiationVersion` are used to determine (with the server) what IRCv3 - //! functionality to enable for the connection. Certain parts of the API offer suggestions for - //! extensions that will improve the user experience, and give examples of how to enable them - //! using `Capability`. `Mode`, `ChannelMode`, and `UserMode` are used in a high-level API for - //! dealing with IRC channel and user modes. They appear in methods for sending mode commands, - //! as well as in the parsed form of received mode commands. - - pub use client::data::Config; - pub use client::reactor::IrcReactor; - pub use client::server::{EachIncomingExt, IrcServer, Server}; - pub use client::server::utils::ServerExt; - pub use proto::{Capability, ChannelExt, Command, Message, NegotiationVersion, Response}; - pub use proto::{ChannelMode, Mode, UserMode}; - - pub use futures::{Future, Stream}; -} diff --git a/src/client/prelude.rs b/src/client/prelude.rs new file mode 100644 index 0000000..9361c37 --- /dev/null +++ b/src/client/prelude.rs @@ -0,0 +1,31 @@ +//! A client-side IRC prelude, re-exporting the complete high-level IRC client API. +//! +//! # Structure +//! A connection to an IRC server is represented by an `IrcServer` which is configured using a +//! `Config` struct that defines data such as which server to connect to, on what port, and +//! using what nickname. The `Server` trait provides an API for actually interacting with the +//! server once a connection has been established. This API intentionally offers only a single +//! method to send `Commands` because it makes it easy to see the whole set of possible +//! interactions with a server. The `ServerExt` trait addresses this deficiency by defining a +//! number of methods that provide a more clear and succinct interface for sending various +//! common IRC commands to the server. An `IrcReactor` can be used to create and manage multiple +//! `IrcServers` with more fine-grained control over error management. +//! +//! The various `proto` types capture details of the IRC protocol that are used throughout the +//! client API. `Message`, `Command`, and `Response` are used to send and receive messages along +//! the connection, and are naturally at the heart of communication in the IRC protocol. +//! `Capability` and `NegotiationVersion` are used to determine (with the server) what IRCv3 +//! functionality to enable for the connection. Certain parts of the API offer suggestions for +//! extensions that will improve the user experience, and give examples of how to enable them +//! using `Capability`. `Mode`, `ChannelMode`, and `UserMode` are used in a high-level API for +//! dealing with IRC channel and user modes. They appear in methods for sending mode commands, +//! as well as in the parsed form of received mode commands. + +pub use client::data::Config; +pub use client::reactor::IrcReactor; +pub use client::server::{EachIncomingExt, IrcServer, Server}; +pub use client::server::utils::ServerExt; +pub use proto::{Capability, ChannelExt, Command, Message, NegotiationVersion, Response}; +pub use proto::{ChannelMode, Mode, UserMode}; + +pub use futures::{Future, Stream}; From 87b84fdeb97a0f5d2df7cebc7b84edc665e2b7f1 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 02:16:50 +0100 Subject: [PATCH 29/31] Changed naming scheme from server to client (resolves #46). --- examples/multiserver.rs | 16 +- examples/reactor.rs | 8 +- examples/reconnector.rs | 16 +- examples/repeater.rs | 10 +- examples/simple.rs | 8 +- examples/simple_ssl.rs | 8 +- examples/tweeter.rs | 10 +- src/client/{server/utils.rs => ext.rs} | 82 +- src/client/mod.rs | 1340 +++++++++++++++++++++++- src/client/prelude.rs | 4 +- src/client/reactor.rs | 86 +- src/lib.rs | 20 +- 12 files changed, 1473 insertions(+), 135 deletions(-) rename src/client/{server/utils.rs => ext.rs} (87%) diff --git a/examples/multiserver.rs b/examples/multiserver.rs index b513946..dbd2030 100644 --- a/examples/multiserver.rs +++ b/examples/multiserver.rs @@ -24,25 +24,25 @@ fn main() { let mut reactor = IrcReactor::new().unwrap(); for config in configs { - // Immediate errors like failure to resolve the server's name or to establish any connection will - // manifest here in the result of prepare_server_and_connect. - let server = reactor.prepare_server_and_connect(&config).unwrap(); - server.identify().unwrap(); - // Here, we tell the reactor to setup this server for future handling (in run) using the specified + // Immediate errors like failure to resolve the client's name or to establish any connection will + // manifest here in the result of prepare_client_and_connect. + let client = reactor.prepare_client_and_connect(&config).unwrap(); + client.identify().unwrap(); + // Here, we tell the reactor to setup this client for future handling (in run) using the specified // handler function process_msg. - reactor.register_server_with_handler(server, process_msg); + reactor.register_client_with_handler(client, process_msg); } // Runtime errors like a dropped connection will manifest here in the result of run. reactor.run().unwrap(); } -fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { +fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { print!("{}", message); match message.command { Command::PRIVMSG(ref target, ref msg) => { if msg.contains("pickles") { - server.send_privmsg(target, "Hi!")?; + client.send_privmsg(target, "Hi!")?; } } _ => (), diff --git a/examples/reactor.rs b/examples/reactor.rs index 0a9f21f..66dea6b 100644 --- a/examples/reactor.rs +++ b/examples/reactor.rs @@ -14,14 +14,14 @@ fn main() { }; let mut reactor = IrcReactor::new().unwrap(); - let server = reactor.prepare_server_and_connect(&config).unwrap(); - server.identify().unwrap(); + let client = reactor.prepare_client_and_connect(&config).unwrap(); + client.identify().unwrap(); - reactor.register_server_with_handler(server, |server, message| { + reactor.register_client_with_handler(client, |client, message| { print!("{}", message); if let Command::PRIVMSG(ref target, ref msg) = message.command { if msg.contains("pickles") { - server.send_privmsg(target, "Hi!")?; + client.send_privmsg(target, "Hi!")?; } } Ok(()) diff --git a/examples/reconnector.rs b/examples/reconnector.rs index 17b0d91..35ad24e 100644 --- a/examples/reconnector.rs +++ b/examples/reconnector.rs @@ -26,17 +26,17 @@ fn main() { loop { let res = configs.iter().fold(Ok(()), |acc, config| { acc.and( - reactor.prepare_server_and_connect(config).and_then(|server| { - server.identify().and(Ok(server)) - }).and_then(|server| { - reactor.register_server_with_handler(server, process_msg); + reactor.prepare_client_and_connect(config).and_then(|client| { + client.identify().and(Ok(client)) + }).and_then(|client| { + reactor.register_client_with_handler(client, process_msg); Ok(()) }) ) }).and_then(|()| reactor.run()); match res { - // The connections ended normally (for example, they sent a QUIT message to the server). + // The connections ended normally (for example, they sent a QUIT message to the client). Ok(_) => break, // Something went wrong! We'll print the error, and restart the connections. Err(e) => eprintln!("{}", e), @@ -44,13 +44,13 @@ fn main() { } } -fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { +fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { print!("{}", message); if let Command::PRIVMSG(ref target, ref msg) = message.command { if msg.contains("pickles") { - server.send_privmsg(target, "Hi!")?; + client.send_privmsg(target, "Hi!")?; } else if msg.contains("quit") { - server.send_quit("bye")?; + client.send_quit("bye")?; } } Ok(()) diff --git a/examples/repeater.rs b/examples/repeater.rs index fa970ef..ea5b7a7 100644 --- a/examples/repeater.rs +++ b/examples/repeater.rs @@ -15,19 +15,19 @@ fn main() { ..Default::default() }; - let server = IrcServer::from_config(config).unwrap(); - server.identify().unwrap(); + let client = IrcClient::from_config(config).unwrap(); + client.identify().unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { print!("{}", message); if let Command::PRIVMSG(ref target, ref msg) = message.command { - if msg.starts_with(server.current_nickname()) { + if msg.starts_with(client.current_nickname()) { let tokens: Vec<_> = msg.split(' ').collect(); if tokens.len() > 2 { let n = tokens[0].len() + tokens[1].len() + 2; if let Ok(count) = tokens[1].parse::() { for _ in 0..count { - server.send_privmsg( + client.send_privmsg( message.response_target().unwrap_or(target), &msg[n..] ).unwrap(); diff --git a/examples/simple.rs b/examples/simple.rs index c063a4f..f6a621b 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -12,14 +12,14 @@ fn main() { ..Default::default() }; - let server = IrcServer::from_config(config).unwrap(); - server.identify().unwrap(); + let client = IrcClient::from_config(config).unwrap(); + client.identify().unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { print!("{}", message); if let Command::PRIVMSG(ref target, ref msg) = message.command { if msg.contains("pickles") { - server.send_privmsg(target, "Hi!").unwrap(); + client.send_privmsg(target, "Hi!").unwrap(); } } }).unwrap(); diff --git a/examples/simple_ssl.rs b/examples/simple_ssl.rs index a3fe915..86ce6f3 100644 --- a/examples/simple_ssl.rs +++ b/examples/simple_ssl.rs @@ -12,14 +12,14 @@ fn main() { ..Default::default() }; - let server = IrcServer::from_config(config).unwrap(); - server.identify().unwrap(); + let client = IrcClient::from_config(config).unwrap(); + client.identify().unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { print!("{}", message); if let Command::PRIVMSG(ref target, ref msg) = message.command { if msg.contains("pickles") { - server.send_privmsg(target, "Hi!").unwrap(); + client.send_privmsg(target, "Hi!").unwrap(); } } }).unwrap(); diff --git a/examples/tweeter.rs b/examples/tweeter.rs index a3f3fdb..94de21a 100644 --- a/examples/tweeter.rs +++ b/examples/tweeter.rs @@ -12,15 +12,15 @@ fn main() { channels: Some(vec!["#irc-crate".to_owned()]), ..Default::default() }; - let server = IrcServer::from_config(config).unwrap(); - server.identify().unwrap(); - let server2 = server.clone(); + let client = IrcClient::from_config(config).unwrap(); + client.identify().unwrap(); + let client2 = client.clone(); // Let's set up a loop that just prints the messages. thread::spawn(move || { - server2.stream().map(|m| print!("{}", m)).wait().count(); + client2.stream().map(|m| print!("{}", m)).wait().count(); }); loop { - server.send_privmsg("#irc-crate", "TWEET TWEET").unwrap(); + client.send_privmsg("#irc-crate", "TWEET TWEET").unwrap(); thread::sleep(Duration::new(10, 0)); } } diff --git a/src/client/server/utils.rs b/src/client/ext.rs similarity index 87% rename from src/client/server/utils.rs rename to src/client/ext.rs index 2ee3e4f..c1bcfc0 100644 --- a/src/client/server/utils.rs +++ b/src/client/ext.rs @@ -1,8 +1,8 @@ //! Utilities and shortcuts for working with IRC servers. //! -//! This module provides the [`ServerExt`](trait.ServerExt.html) trait which is the idiomatic way of +//! This module provides the [`ClientExt`](trait.ClientExt.html) trait which is the idiomatic way of //! sending messages to an IRC server. This trait is automatically implemented for everything that -//! implements [`Server`](../trait.Server.html) and is designed to provide important functionality +//! implements [`Client`](../trait.Client.html) and is designed to provide important functionality //! without clutter. //! //! # Examples @@ -12,17 +12,17 @@ //! //! ```no_run //! # extern crate irc; -//! use irc::client::prelude::{IrcServer, ServerExt}; +//! use irc::client::prelude::{IrcClient, ClientExt}; //! //! # fn main() { -//! let server = IrcServer::new("config.toml").unwrap(); -//! // identify and send_privmsg both come from `ServerExt` +//! let server = IrcClient::new("config.toml").unwrap(); +//! // identify and send_privmsg both come from `ClientExt` //! server.identify().unwrap(); //! server.send_privmsg("#example", "Hello, world!").unwrap(); //! # } //! ``` //! -//! `ServerExt::identify` also plays an important role in performing IRCv3 capability negotiations. +//! `ClientExt::identify` also plays an important role in performing IRCv3 capability negotiations. //! In particular, calling `identify` will close the negotiations (and otherwise indicate IRCv3 //! compatibility). This means that all IRCv3 capability requests should be performed before calling //! `identify`. For example: @@ -31,7 +31,7 @@ //! # extern crate irc; //! # use irc::client::prelude::*; //! # fn main() { -//! # let server = IrcServer::new("config.toml").unwrap(); +//! # let server = IrcClient::new("config.toml").unwrap(); //! server.send_cap_req(&[Capability::MultiPrefix, Capability::UserhostInNames]).unwrap(); //! server.identify().unwrap(); //! # } @@ -46,10 +46,10 @@ use proto::{Capability, Command, Mode, NegotiationVersion}; use proto::command::CapSubCommand::{END, LS, REQ}; use proto::command::Command::*; use proto::mode::ModeType; -use client::server::Server; +use client::Client; /// Idiomatic extensions for sending messages to an IRC server. -pub trait ServerExt: Server { +pub trait ClientExt: Client { /// Sends a request for a list of server capabilities for a specific IRCv3 version. fn send_cap_ls(&self, version: NegotiationVersion) -> Result<()> where @@ -377,23 +377,23 @@ pub trait ServerExt: Server { } } -impl ServerExt for S +impl ClientExt for S where - S: Server, + S: Client, { } #[cfg(test)] mod test { - use super::ServerExt; + use super::ClientExt; use client::data::Config; - use client::server::IrcServer; - use client::server::test::{get_server_value, test_config}; + use client::IrcClient; + use client::test::{get_server_value, test_config}; use proto::{ChannelMode, Mode}; #[test] fn identify() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.identify().unwrap(); assert_eq!( &get_server_value(server)[..], @@ -404,7 +404,7 @@ mod test { #[test] fn identify_with_password() { - let server = IrcServer::from_config(Config { + let server = IrcClient::from_config(Config { nickname: Some(format!("test")), password: Some(format!("password")), ..test_config() @@ -419,14 +419,14 @@ mod test { #[test] fn send_pong() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_pong("irc.test.net").unwrap(); assert_eq!(&get_server_value(server)[..], "PONG :irc.test.net\r\n"); } #[test] fn send_join() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_join("#test,#test2,#test3").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -436,21 +436,21 @@ mod test { #[test] fn send_part() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_part("#test").unwrap(); assert_eq!(&get_server_value(server)[..], "PART #test\r\n"); } #[test] fn send_oper() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_oper("test", "test").unwrap(); assert_eq!(&get_server_value(server)[..], "OPER test :test\r\n"); } #[test] fn send_privmsg() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_privmsg("#test", "Hi, everybody!").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -460,7 +460,7 @@ mod test { #[test] fn send_notice() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_notice("#test", "Hi, everybody!").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -470,14 +470,14 @@ mod test { #[test] fn send_topic_no_topic() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_topic("#test", "").unwrap(); assert_eq!(&get_server_value(server)[..], "TOPIC #test\r\n"); } #[test] fn send_topic() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_topic("#test", "Testing stuff.").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -487,7 +487,7 @@ mod test { #[test] fn send_kill() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_kill("test", "Testing kills.").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -497,14 +497,14 @@ mod test { #[test] fn send_kick_no_message() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_kick("#test", "test", "").unwrap(); assert_eq!(&get_server_value(server)[..], "KICK #test test\r\n"); } #[test] fn send_kick() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_kick("#test", "test", "Testing kicks.").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -514,14 +514,14 @@ mod test { #[test] fn send_mode_no_modeparams() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap(); assert_eq!(&get_server_value(server)[..], "MODE #test +i\r\n"); } #[test] fn send_mode() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))]) .unwrap(); assert_eq!(&get_server_value(server)[..], "MODE #test +o test\r\n"); @@ -529,28 +529,28 @@ mod test { #[test] fn send_samode_no_modeparams() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_samode("#test", "+i", "").unwrap(); assert_eq!(&get_server_value(server)[..], "SAMODE #test +i\r\n"); } #[test] fn send_samode() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_samode("#test", "+o", "test").unwrap(); assert_eq!(&get_server_value(server)[..], "SAMODE #test +o test\r\n"); } #[test] fn send_sanick() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_sanick("test", "test2").unwrap(); assert_eq!(&get_server_value(server)[..], "SANICK test test2\r\n"); } #[test] fn send_invite() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_invite("test", "#test").unwrap(); assert_eq!(&get_server_value(server)[..], "INVITE test #test\r\n"); } @@ -558,7 +558,7 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_ctcp() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_ctcp("test", "MESSAGE").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -569,7 +569,7 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_action() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_action("test", "tests.").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -580,7 +580,7 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_finger() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_finger("test").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -591,7 +591,7 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_version() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_version("test").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -602,7 +602,7 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_source() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_source("test").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -613,7 +613,7 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_user_info() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_user_info("test").unwrap(); assert_eq!( &get_server_value(server)[..], @@ -624,7 +624,7 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_ctcp_ping() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_ctcp_ping("test").unwrap(); let val = get_server_value(server); println!("{}", val); @@ -635,7 +635,7 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_time() { - let server = IrcServer::from_config(test_config()).unwrap(); + let server = IrcClient::from_config(test_config()).unwrap(); server.send_time("test").unwrap(); assert_eq!( &get_server_value(server)[..], diff --git a/src/client/mod.rs b/src/client/mod.rs index f8b6e6e..fa74b18 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,9 +1,1347 @@ //! A simple, thread-safe, and async-friendly IRC client library. +//! +//! This API provides the ability to connect to an IRC server via the +//! [`IrcClient`](struct.IrcClient.html) type. The [`Client`](trait.Client.html) trait that +//! [`IrcClient`](struct.IrcClient.html) implements provides methods for communicating with this +//! server. An extension trait, [`ClientExt`](./utils/trait.ClientExt.html), provides short-hand for +//! sending a variety of important messages without referring to their entries in +//! [`proto::command`](../../proto/command/enum.Command.html). +//! +//! # Examples +//! +//! Using these APIs, we can connect to a server and send a one-off message (in this case, +//! identifying with the server). +//! +//! ```no_run +//! # extern crate irc; +//! use irc::client::prelude::{IrcClient, ClientExt}; +//! +//! # fn main() { +//! let server = IrcClient::new("config.toml").unwrap(); +//! // identify comes from `ClientExt` +//! server.identify().unwrap(); +//! # } +//! ``` +//! +//! We can then use functions from [Client](trait.Client.html) to receive messages from the +//! server in a blocking fashion and perform any desired actions in response. The following code +//! performs a simple call-and-response when the bot's name is mentioned in a channel. +//! +//! ```no_run +//! # extern crate irc; +//! # use irc::client::prelude::{IrcClient, ClientExt}; +//! use irc::client::prelude::{Client, Command}; +//! +//! # fn main() { +//! # let server = IrcClient::new("config.toml").unwrap(); +//! # server.identify().unwrap(); +//! server.for_each_incoming(|irc_msg| { +//! match irc_msg.command { +//! Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) { +//! server.send_privmsg(&channel, "beep boop").unwrap(); +//! } +//! _ => () +//! } +//! }).unwrap(); +//! # } +//! ``` + +#[cfg(feature = "ctcp")] +use std::ascii::AsciiExt; +use std::collections::HashMap; +use std::path::Path; +use std::sync::{Arc, Mutex, RwLock}; +use std::thread; + +#[cfg(feature = "ctcp")] +use chrono::prelude::*; +use futures::{Async, Poll, Future, Sink, Stream}; +use futures::stream::SplitStream; +use futures::sync::mpsc; +use futures::sync::oneshot; +use futures::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio_core::reactor::{Core, Handle}; + +use error; +use client::conn::{Connection, ConnectionFuture}; +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}; pub mod conn; pub mod data; +pub mod ext; pub mod prelude; pub mod reactor; -pub mod server; pub mod transport; +/// Trait extending all IRC streams with `for_each_incoming` convenience function. +/// +/// This is typically used in conjunction with [`Client::stream`](trait.Client.html#tymethod.stream) +/// in order to use an API akin to +/// [`Client::for_each_incoming`](trait.Client.html#method.for_each_incoming). +/// +/// # Example +/// +/// ```no_run +/// # extern crate irc; +/// # use irc::client::prelude::{IrcClient, Client, Command, ClientExt}; +/// use irc::client::prelude::EachIncomingExt; +/// +/// # fn main() { +/// # let server = IrcClient::new("config.toml").unwrap(); +/// # server.identify().unwrap(); +/// server.stream().for_each_incoming(|irc_msg| { +/// match irc_msg.command { +/// Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) { +/// server.send_privmsg(&channel, "beep boop").unwrap(); +/// } +/// _ => () +/// } +/// }).unwrap(); +/// # } +/// ``` +pub trait EachIncomingExt: Stream { + /// Blocks on the stream, running the given function on each incoming message as they arrive. + fn for_each_incoming(self, mut f: F) -> error::Result<()> + where + F: FnMut(Message) -> (), + Self: Sized, + { + self.for_each(|msg| { + f(msg); + Ok(()) + }).wait() + } +} + +impl EachIncomingExt for T where T: Stream {} + +/// An interface for communicating with an IRC server. +pub trait Client { + /// Gets the configuration being used with this Client. + fn config(&self) -> &Config; + + /// Sends a [Command](../../proto/command/enum.Command.html) to this Client. This is the core + /// primitive for sending messages to the server. In practice, it's often more pleasant (and + /// more idiomatic) to use the functions defined on [ClientExt](./utils/trait.ClientExt.html). + /// They capture a lot of the more repetitive aspects of sending messages. + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use irc::client::prelude::*; + /// # fn main() { + /// # let server = IrcClient::new("config.toml").unwrap(); + /// server.send(Command::NICK("example".to_owned())).unwrap(); + /// server.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap(); + /// # } + /// ``` + fn send>(&self, message: M) -> error::Result<()> + where + Self: Sized; + + /// Gets a stream of incoming messages from the Client. This is only necessary when trying to + /// set up more complex clients, and requires use of the `futures` crate. Most IRC bots should + /// be able to get by using only `for_each_incoming` to handle received messages. You can find + /// some examples of more complex setups using `stream` in the + /// [GitHub repository](https://github.com/aatxe/irc/tree/master/examples). + /// + /// **Note**: The stream can only be returned once. Subsequent attempts will cause a panic. + fn stream(&self) -> ClientStream; + + /// Blocks on the stream, running the given function on each incoming message as they arrive. + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use irc::client::prelude::{IrcClient, ClientExt, Client, Command}; + /// # fn main() { + /// # let server = IrcClient::new("config.toml").unwrap(); + /// # server.identify().unwrap(); + /// server.for_each_incoming(|irc_msg| { + /// match irc_msg.command { + /// Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) { + /// server.send_privmsg(&channel, "beep boop").unwrap(); + /// } + /// _ => () + /// } + /// }).unwrap(); + /// # } + /// ``` + fn for_each_incoming(&self, f: F) -> error::Result<()> + where + F: FnMut(Message) -> (), + { + self.stream().for_each_incoming(f) + } + + /// Gets a list of currently joined channels. This will be `None` if tracking is disabled + /// altogether via the `nochanlists` feature. + fn list_channels(&self) -> Option>; + + /// Gets a list of [Users](../data/user/struct.User.html) in the specified channel. If the + /// specified channel hasn't been joined or the `nochanlists` feature is enabled, this function + /// will return `None`. + /// + /// For best results, be sure to request `multi-prefix` support from the server. This will allow + /// for more accurate tracking of user rank (e.g. oper, half-op, etc.). + /// # Requesting multi-prefix support + /// ```no_run + /// # extern crate irc; + /// # use irc::client::prelude::{IrcClient, ClientExt, Client, Command}; + /// use irc::proto::caps::Capability; + /// + /// # fn main() { + /// # let server = IrcClient::new("config.toml").unwrap(); + /// server.send_cap_req(&[Capability::MultiPrefix]).unwrap(); + /// server.identify().unwrap(); + /// # } + /// ``` + fn list_users(&self, channel: &str) -> Option>; +} + +/// A stream of `Messages` from the `IrcClient`. +/// +/// Interaction with this stream relies on the `futures` API, but is only expected for less +/// traditional use cases. To learn more, you can view the documentation for the +/// [futures](https://docs.rs/futures/) crate, or the tutorials for +/// [tokio](https://tokio.rs/docs/getting-started/futures/). +#[derive(Debug)] +pub struct ClientStream { + state: Arc, + stream: SplitStream, +} + +impl Stream for ClientStream { + type Item = Message; + type Error = error::IrcError; + + fn poll(&mut self) -> Poll, Self::Error> { + match try_ready!(self.stream.poll()) { + Some(msg) => { + self.state.handle_message(&msg)?; + Ok(Async::Ready(Some(msg))) + } + None => Ok(Async::Ready(None)), + } + } +} + +/// Thread-safe internal state for an IRC server connection. +#[derive(Debug)] +struct ClientState { + /// The configuration used with this connection. + config: Config, + /// A thread-safe map of channels to the list of users in them. + chanlists: Mutex>>, + /// A thread-safe index to track the current alternative nickname being used. + alt_nick_index: RwLock, + /// A thread-safe internal IRC stream used for the reading API. + incoming: Mutex>>, + /// A thread-safe copy of the outgoing channel. + outgoing: UnboundedSender, +} + +impl<'a> Client for ClientState { + fn config(&self) -> &Config { + &self.config + } + + 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(), + )?) + } + + fn stream(&self) -> ClientStream { + unimplemented!() + } + + #[cfg(not(feature = "nochanlists"))] + fn list_channels(&self) -> Option> { + Some( + self.chanlists + .lock() + .unwrap() + .keys() + .map(|k| k.to_owned()) + .collect(), + ) + } + + #[cfg(feature = "nochanlists")] + fn list_channels(&self) -> Option> { + None + } + + #[cfg(not(feature = "nochanlists"))] + fn list_users(&self, chan: &str) -> Option> { + self.chanlists + .lock() + .unwrap() + .get(&chan.to_owned()) + .cloned() + } + + #[cfg(feature = "nochanlists")] + fn list_users(&self, _: &str) -> Option> { + None + } +} + +impl ClientState { + fn new( + incoming: SplitStream, + outgoing: UnboundedSender, + config: Config, + ) -> ClientState { + ClientState { + config: config, + chanlists: Mutex::new(HashMap::new()), + alt_nick_index: RwLock::new(0), + incoming: Mutex::new(Some(incoming)), + outgoing: outgoing, + } + } + + /// 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(); + let index = self.alt_nick_index.read().unwrap(); + match *index { + 0 => self.config().nickname().expect( + "current_nickname should not be callable if nickname is not defined." + ), + i => alt_nicks[i - 1], + } + } + + /// Handles sent messages internally for basic client functionality. + fn handle_sent_message(&self, msg: &Message) -> error::Result<()> { + trace!("[SENT] {}", msg.to_string()); + match msg.command { + PART(ref chan, _) => { + let _ = self.chanlists.lock().unwrap().remove(chan); + } + _ => (), + } + Ok(()) + } + + /// Handles received messages internally for basic client functionality. + fn handle_message(&self, msg: &Message) -> error::Result<()> { + trace!("[RECV] {}", msg.to_string()); + 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), + QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")), + NICK(ref new_nick) => { + self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick) + } + ChannelMODE(ref chan, ref modes) => self.handle_mode(chan, modes), + PRIVMSG(ref target, ref body) => { + if body.starts_with('\u{001}') { + let tokens: Vec<_> = { + let end = if body.ends_with('\u{001}') { + body.len() - 1 + } else { + body.len() + }; + body[1..end].split(' ').collect() + }; + if target.starts_with('#') { + self.handle_ctcp(target, &tokens)? + } else if let Some(user) = msg.source_nickname() { + self.handle_ctcp(user, &tokens)? + } + } + } + Command::Response(Response::RPL_NAMREPLY, ref args, ref suffix) => { + self.handle_namreply(args, suffix) + } + Command::Response(Response::RPL_ENDOFMOTD, _, _) | + Command::Response(Response::ERR_NOMOTD, _, _) => { + self.send_nick_password()?; + self.send_umodes()?; + + 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)?, + None => self.send_join(chan)?, + } + } + let joined_chans = self.chanlists.lock().unwrap(); + for chan in joined_chans.keys().filter( + |x| !config_chans.contains(&x.as_str()), + ) + { + self.send_join(chan)? + } + } + Command::Response(Response::ERR_NICKNAMEINUSE, _, _) | + Command::Response(Response::ERR_ERRONEOUSNICKNAME, _, _) => { + let alt_nicks = self.config().alternate_nicknames(); + let mut index = self.alt_nick_index.write().unwrap(); + if *index >= alt_nicks.len() { + panic!("All specified nicknames were in use or disallowed.") + } else { + self.send(NICK(alt_nicks[*index].to_owned()))?; + *index += 1; + } + } + _ => (), + } + Ok(()) + } + + fn send_nick_password(&self) -> error::Result<()> { + if self.config().nick_password().is_empty() { + Ok(()) + } else { + let mut index = self.alt_nick_index.write().unwrap(); + if self.config().should_ghost() && *index != 0 { + for seq in &self.config().ghost_sequence() { + self.send(NICKSERV(format!( + "{} {} {}", + seq, + self.config().nickname()?, + self.config().nick_password() + )))?; + } + *index = 0; + self.send(NICK(self.config().nickname()?.to_owned()))? + } + self.send(NICKSERV( + format!("IDENTIFY {}", self.config().nick_password()), + )) + } + } + + fn send_umodes(&self) -> error::Result<()> { + if self.config().umodes().is_empty() { + Ok(()) + } else { + self.send_mode( + self.current_nickname(), &Mode::as_user_modes(self.config().umodes()).map_err(|e| { + error::IrcError::InvalidMessage { + string: format!( + "MODE {} {}", self.current_nickname(), self.config().umodes() + ), + cause: e, + } + })? + ) + } + } + + #[cfg(feature = "nochanlists")] + fn handle_join(&self, _: &str, _: &str) {} + + #[cfg(not(feature = "nochanlists"))] + fn handle_join(&self, src: &str, chan: &str) { + if let Some(vec) = self.chanlists.lock().unwrap().get_mut(&chan.to_owned()) { + if !src.is_empty() { + vec.push(User::new(src)) + } + } + } + + #[cfg(feature = "nochanlists")] + fn handle_part(&self, src: &str, chan: &str) {} + + #[cfg(not(feature = "nochanlists"))] + fn handle_part(&self, src: &str, chan: &str) { + if let Some(vec) = self.chanlists.lock().unwrap().get_mut(&chan.to_owned()) { + if !src.is_empty() { + if let Some(n) = vec.iter().position(|x| x.get_nickname() == src) { + vec.swap_remove(n); + } + } + } + } + + #[cfg(feature = "nochanlists")] + fn handle_quit(&self, _: &str) {} + + #[cfg(not(feature = "nochanlists"))] + fn handle_quit(&self, src: &str) { + if src.is_empty() { + return; + } + let mut chanlists = self.chanlists.lock().unwrap(); + for channel in chanlists.clone().keys() { + if let Some(vec) = chanlists.get_mut(&channel.to_owned()) { + if let Some(p) = vec.iter().position(|x| x.get_nickname() == src) { + vec.swap_remove(p); + } + } + } + } + + #[cfg(feature = "nochanlists")] + fn handle_nick_change(&self, _: &str, _: &str) {} + + #[cfg(not(feature = "nochanlists"))] + fn handle_nick_change(&self, old_nick: &str, new_nick: &str) { + if old_nick.is_empty() || new_nick.is_empty() { + return; + } + let mut chanlists = self.chanlists.lock().unwrap(); + for channel in chanlists.clone().keys() { + if let Some(vec) = chanlists.get_mut(&channel.to_owned()) { + if let Some(n) = vec.iter().position(|x| x.get_nickname() == old_nick) { + let new_entry = User::new(new_nick); + vec[n] = new_entry; + } + } + } + } + + #[cfg(feature = "nochanlists")] + fn handle_mode(&self, _: &str, _: &[Mode]) {} + + #[cfg(not(feature = "nochanlists"))] + fn handle_mode(&self, chan: &str, modes: &[Mode]) { + for mode in modes { + match *mode { + Mode::Plus(_, Some(ref user)) | Mode::Minus(_, Some(ref user)) => { + if let Some(vec) = self.chanlists.lock().unwrap().get_mut(chan) { + if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) { + vec[n].update_access_level(mode) + } + } + } + _ => (), + } + } + } + + #[cfg(feature = "nochanlists")] + fn handle_namreply(&self, _: &[String], _: &Option) {} + + #[cfg(not(feature = "nochanlists"))] + fn handle_namreply(&self, args: &[String], suffix: &Option) { + if let Some(ref users) = *suffix { + if args.len() == 3 { + let chan = &args[2]; + for user in users.split(' ') { + let mut chanlists = self.chanlists.lock().unwrap(); + chanlists + .entry(chan.clone()) + .or_insert_with(Vec::new) + .push(User::new(user)) + } + } + } + } + + #[cfg(feature = "ctcp")] + fn handle_ctcp(&self, resp: &str, tokens: &[&str]) -> error::Result<()> { + if tokens.is_empty() { + return Ok(()); + } + if tokens[0].eq_ignore_ascii_case("FINGER") { + self.send_ctcp_internal( + resp, + &format!( + "FINGER :{} ({})", + self.config().real_name(), + self.config().username() + ), + ) + } else if tokens[0].eq_ignore_ascii_case("VERSION") { + self.send_ctcp_internal(resp, &format!("VERSION {}", self.config().version())) + } else if tokens[0].eq_ignore_ascii_case("SOURCE") { + self.send_ctcp_internal( + resp, + &format!("SOURCE {}", self.config().source()), + )?; + self.send_ctcp_internal(resp, "SOURCE") + } else if tokens[0].eq_ignore_ascii_case("PING") && tokens.len() > 1 { + self.send_ctcp_internal(resp, &format!("PING {}", tokens[1])) + } else if tokens[0].eq_ignore_ascii_case("TIME") { + self.send_ctcp_internal(resp, &format!("TIME :{}", Local::now().to_rfc2822())) + } else if tokens[0].eq_ignore_ascii_case("USERINFO") { + self.send_ctcp_internal(resp, &format!("USERINFO :{}", self.config().user_info())) + } else { + Ok(()) + } + } + + #[cfg(feature = "ctcp")] + fn send_ctcp_internal(&self, target: &str, msg: &str) -> error::Result<()> { + self.send_notice(target, &format!("\u{001}{}\u{001}", msg)) + } + + #[cfg(not(feature = "ctcp"))] + fn handle_ctcp(&self, _: &str, _: &[&str]) -> error::Result<()> { + Ok(()) + } +} + +/// The canonical implementation of a connection to an IRC server. +/// +/// The type itself provides a number of methods to create new connections, but most of the API +/// surface is in the form of the [Client](trait.Client.html) and +/// [`ClientExt`](./utils/trait.ClientExt.html) traits that provide methods of communicating with +/// the server after connection. Cloning an `IrcClient` is relatively cheap, as it's equivalent to +/// cloning a single `Arc`. This may be useful for setting up multiple threads with access to one +/// connection. +/// +/// For a full example usage, see [`irc::client::server`](./index.html). +#[derive(Clone, Debug)] +pub struct IrcClient { + /// The internal, thread-safe server state. + state: Arc, + /// A view of the logs for a mock connection. + view: Option, +} + +impl Client for IrcClient { + fn config(&self) -> &Config { + &self.state.config + } + + fn send>(&self, msg: M) -> error::Result<()> + where + Self: Sized, + { + self.state.send(msg) + } + + fn stream(&self) -> ClientStream { + ClientStream { + state: Arc::clone(&self.state), + stream: self.state.incoming.lock().unwrap().take().expect( + "Stream was already obtained once, and cannot be reobtained." + ), + } + } + + #[cfg(not(feature = "nochanlists"))] + fn list_channels(&self) -> Option> { + Some( + self.state + .chanlists + .lock() + .unwrap() + .keys() + .map(|k| k.to_owned()) + .collect(), + ) + } + + #[cfg(feature = "nochanlists")] + fn list_channels(&self) -> Option> { + None + } + + #[cfg(not(feature = "nochanlists"))] + fn list_users(&self, chan: &str) -> Option> { + self.state + .chanlists + .lock() + .unwrap() + .get(&chan.to_owned()) + .cloned() + } + + #[cfg(feature = "nochanlists")] + fn list_users(&self, _: &str) -> Option> { + None + } +} + +impl IrcClient { + /// Creates a new IRC Client connection from the configuration at the specified path, connecting + /// immediately. This function is short-hand for loading the configuration and then calling + /// `IrcClient::from_config` and consequently inherits its behaviors. + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use irc::client::prelude::*; + /// # fn main() { + /// let server = IrcClient::new("config.toml").unwrap(); + /// # } + /// ``` + pub fn new>(config: P) -> error::Result { + IrcClient::from_config(Config::load(config)?) + } + + /// Creates a new IRC server connection from the specified configuration, connecting + /// immediately. Due to current design limitations, error handling here is somewhat limited. In + /// particular, failed connections will cause the program to panic because the connection + /// attempt is made on a freshly created thread. If you need to avoid this behavior and handle + /// errors more gracefully, it is recommended that you use an + /// [IrcReactor](../reactor/struct.IrcReactor.html) instead. + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # use std::default::Default; + /// # use irc::client::prelude::*; + /// # fn main() { + /// let config = Config { + /// nickname: Some("example".to_owned()), + /// server: Some("irc.example.com".to_owned()), + /// .. Default::default() + /// }; + /// let server = IrcClient::from_config(config).unwrap(); + /// # } + /// ``` + pub fn from_config(config: Config) -> error::Result { + // Setting up a remote reactor running for the length of the connection. + let (tx_outgoing, rx_outgoing) = mpsc::unbounded(); + let (tx_incoming, rx_incoming) = oneshot::channel(); + let (tx_view, rx_view) = oneshot::channel(); + + let mut reactor = Core::new()?; + let handle = reactor.handle(); + // Attempting to connect here (as opposed to on the thread) allows more errors to happen + // immediately, rather than to occur as panics on the thread. In particular, non-resolving + // server names, and failed SSL setups will appear here. + let conn = reactor.run(Connection::new(&config, &handle)?)?; + + let _ = thread::spawn(move || { + let mut reactor = Core::new().unwrap(); + + tx_view.send(conn.log_view()).unwrap(); + let (sink, stream) = conn.split(); + + let outgoing_future = sink.send_all(rx_outgoing.map_err::(|_| { + unreachable!("futures::sync::mpsc::Receiver should never return Err"); + })).map(|_| ()).map_err(|e| panic!("{}", e)); + + // Send the stream half back to the original thread. + tx_incoming.send(stream).unwrap(); + + reactor.run(outgoing_future).unwrap(); + }); + + Ok(IrcClient { + state: Arc::new(ClientState::new(rx_incoming.wait()?, tx_outgoing, config)), + view: rx_view.wait()?, + }) + } + + /// Creates a new IRC server connection from the specified configuration and on the event loop + /// corresponding to the given handle. This can be used to set up a number of IrcClients on a + /// single, shared event loop. It can also be used to take more control over execution and error + /// handling. Connection will not occur until the event loop is run. + /// + /// Proper usage requires familiarity with `tokio` and `futures`. You can find more information + /// in the crate documentation for [tokio-core](http://docs.rs/tokio-core) or + /// [futures](http://docs.rs/futures). Additionally, you can find detailed tutorials on using + /// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). An easy + /// to use abstraction that does not require this knowledge is available via + /// [`IrcReactors`](../reactor/struct.IrcReactor.html). + /// + /// # Example + /// ```no_run + /// # extern crate irc; + /// # extern crate tokio_core; + /// # use std::default::Default; + /// # use irc::client::prelude::*; + /// # use irc::client::PackedIrcClient; + /// # use irc::error; + /// # use tokio_core::reactor::Core; + /// # fn main() { + /// # let config = Config { + /// # nickname: Some("example".to_owned()), + /// # server: Some("irc.example.com".to_owned()), + /// # .. Default::default() + /// # }; + /// let mut reactor = Core::new().unwrap(); + /// let future = IrcClient::new_future(reactor.handle(), &config).unwrap(); + /// // immediate connection errors (like no internet) will turn up here... + /// let PackedIrcClient(server, future) = reactor.run(future).unwrap(); + /// // runtime errors (like disconnections and so forth) will turn up here... + /// reactor.run(server.stream().for_each(move |irc_msg| { + /// // processing messages works like usual + /// process_msg(&server, irc_msg) + /// }).join(future)).unwrap(); + /// # } + /// # fn process_msg(server: &IrcClient, message: Message) -> error::Result<()> { Ok(()) } + /// ``` + pub fn new_future(handle: Handle, config: &Config) -> error::Result { + let (tx_outgoing, rx_outgoing) = mpsc::unbounded(); + + Ok(IrcClientFuture { + conn: Connection::new(config, &handle)?, + _handle: handle, + config: config, + tx_outgoing: Some(tx_outgoing), + rx_outgoing: Some(rx_outgoing), + }) + } + + /// Gets the current nickname in use. This may be the primary username set in the configuration, + /// or it could be any of the alternative nicknames listed as well. As a result, this is the + /// preferred way to refer to the client's nickname. + pub fn current_nickname(&self) -> &str { + self.state.current_nickname() + } + + /// Gets the log view from the internal transport. Only used for unit testing. + #[cfg(test)] + fn log_view(&self) -> &LogView { + self.view.as_ref().unwrap() + } +} + +/// A future representing the eventual creation of an `IrcClient`. +/// +/// Interaction with this future relies on the `futures` API, but is only expected for more advanced +/// use cases. To learn more, you can view the documentation for the +/// [futures](https://docs.rs/futures/) crate, or the tutorials for +/// [tokio](https://tokio.rs/docs/getting-started/futures/). An easy to use abstraction that does +/// not require this knowledge is available via [`IrcReactors`](../reactor/struct.IrcReactor.html). + #[derive(Debug)] +pub struct IrcClientFuture<'a> { + conn: ConnectionFuture<'a>, + _handle: Handle, + config: &'a Config, + tx_outgoing: Option>, + rx_outgoing: Option>, +} + +impl<'a> Future for IrcClientFuture<'a> { + type Item = PackedIrcClient; + type Error = error::IrcError; + + fn poll(&mut self) -> Poll { + let conn = try_ready!(self.conn.poll()); + + let view = conn.log_view(); + let (sink, stream) = conn.split(); + + let outgoing_future = sink.send_all( + self.rx_outgoing.take().unwrap().map_err::(|()| { + unreachable!("futures::sync::mpsc::Receiver should never return Err"); + }) + ).map(|_| ()); + + let server = IrcClient { + state: Arc::new(ClientState::new( + stream, self.tx_outgoing.take().unwrap(), self.config.clone() + )), + view: view, + }; + Ok(Async::Ready(PackedIrcClient(server, Box::new(outgoing_future)))) + } +} + +/// An `IrcClient` packaged with a future that drives its message sending. In order for the server +/// to actually work properly, this future _must_ be running. +/// +/// This type should only be used by advanced users who are familiar with the implementation of this +/// crate. An easy to use abstraction that does not require this knowledge is available via +/// [`IrcReactors`](../reactor/struct.IrcReactor.html). +pub struct PackedIrcClient(pub IrcClient, pub Box>); + +#[cfg(test)] +mod test { + use std::collections::HashMap; + use std::default::Default; + use std::thread; + use std::time::Duration; + + use super::{IrcClient, Client}; + use client::data::Config; + #[cfg(not(feature = "nochanlists"))] + use client::data::User; + use proto::{ChannelMode, Mode}; + use proto::command::Command::{PART, PRIVMSG}; + + pub fn test_config() -> Config { + Config { + owners: Some(vec![format!("test")]), + nickname: Some(format!("test")), + alt_nicks: Some(vec![format!("test2")]), + server: Some(format!("irc.test.net")), + channels: Some(vec![format!("#test"), format!("#test2")]), + user_info: Some(format!("Testing.")), + use_mock_connection: Some(true), + ..Default::default() + } + } + + pub fn get_server_value(server: IrcClient) -> String { + // We sleep here because of synchronization issues. + // We can't guarantee that everything will have been sent by the time of this call. + thread::sleep(Duration::from_millis(100)); + server.log_view().sent().unwrap().iter().fold(String::new(), |mut acc, msg| { + acc.push_str(&msg.to_string()); + acc + }) + } + + #[test] + fn stream() { + let exp = "PRIVMSG test :Hi!\r\nPRIVMSG test :This is a test!\r\n\ + :test!test@test JOIN #test\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(exp.to_owned()), + ..test_config() + }).unwrap(); + let mut messages = String::new(); + server.for_each_incoming(|message| { + messages.push_str(&message.to_string()); + }).unwrap(); + assert_eq!(&messages[..], exp); + } + + #[test] + fn handle_message() { + let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "JOIN #test\r\nJOIN #test2\r\n" + ); + } + + #[test] + fn handle_end_motd_with_nick_password() { + let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + nick_password: Some(format!("password")), + channels: Some(vec![format!("#test"), format!("#test2")]), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "NICKSERV IDENTIFY password\r\nJOIN #test\r\n\ + JOIN #test2\r\n" + ); + } + + #[test] + fn handle_end_motd_with_chan_keys() { + let value = ":irc.test.net 376 test :End of /MOTD command\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + nickname: Some(format!("test")), + channels: Some(vec![format!("#test"), format!("#test2")]), + channel_keys: { + let mut map = HashMap::new(); + map.insert(format!("#test2"), format!("password")); + Some(map) + }, + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "JOIN #test\r\nJOIN #test2 password\r\n" + ); + } + + #[test] + fn handle_end_motd_with_ghost() { + let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\ + :irc.test.net 376 test2 :End of /MOTD command.\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + nickname: Some(format!("test")), + alt_nicks: Some(vec![format!("test2")]), + nick_password: Some(format!("password")), + channels: Some(vec![format!("#test"), format!("#test2")]), + should_ghost: Some(true), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "NICK :test2\r\nNICKSERV GHOST test password\r\n\ + NICK :test\r\nNICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n" + ); + } + + #[test] + fn handle_end_motd_with_ghost_seq() { + let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\ + :irc.test.net 376 test2 :End of /MOTD command.\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + nickname: Some(format!("test")), + alt_nicks: Some(vec![format!("test2")]), + nick_password: Some(format!("password")), + channels: Some(vec![format!("#test"), format!("#test2")]), + should_ghost: Some(true), + ghost_sequence: Some(vec![format!("RECOVER"), format!("RELEASE")]), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "NICK :test2\r\nNICKSERV RECOVER test password\ + \r\nNICKSERV RELEASE test password\r\nNICK :test\r\nNICKSERV IDENTIFY password\ + \r\nJOIN #test\r\nJOIN #test2\r\n" + ); + } + + #[test] + fn handle_end_motd_with_umodes() { + let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + nickname: Some(format!("test")), + umodes: Some(format!("+B")), + channels: Some(vec![format!("#test"), format!("#test2")]), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "MODE test +B\r\nJOIN #test\r\nJOIN #test2\r\n" + ); + } + + #[test] + fn nickname_in_use() { + let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!(&get_server_value(server)[..], "NICK :test2\r\n"); + } + + #[test] + #[should_panic(expected = "All specified nicknames were in use or disallowed.")] + fn ran_out_of_nicknames() { + let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\ + :irc.pdgn.co 433 * test2 :Nickname is already in use.\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + } + + #[test] + fn send() { + let server = IrcClient::from_config(test_config()).unwrap(); + assert!( + server + .send(PRIVMSG(format!("#test"), format!("Hi there!"))) + .is_ok() + ); + assert_eq!( + &get_server_value(server)[..], + "PRIVMSG #test :Hi there!\r\n" + ); + } + + #[test] + fn send_no_newline_injection() { + let server = IrcClient::from_config(test_config()).unwrap(); + assert!( + server + .send(PRIVMSG(format!("#test"), format!("Hi there!\r\nJOIN #bad"))) + .is_ok() + ); + assert_eq!(&get_server_value(server)[..], "PRIVMSG #test :Hi there!\r\n"); + } + + #[test] + #[cfg(not(feature = "nochanlists"))] + fn channel_tracking_names() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!(server.list_channels().unwrap(), vec!["#test".to_owned()]) + } + + #[test] + #[cfg(not(feature = "nochanlists"))] + fn channel_tracking_names_part() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert!(server.send(PART(format!("#test"), None)).is_ok()); + assert!(server.list_channels().unwrap().is_empty()) + } + + #[test] + #[cfg(not(feature = "nochanlists"))] + fn user_tracking_names() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + server.list_users("#test").unwrap(), + vec![User::new("test"), User::new("~owner"), User::new("&admin")] + ) + } + + #[test] + #[cfg(not(feature = "nochanlists"))] + fn user_tracking_names_join() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ + :test2!test@test JOIN #test\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + server.list_users("#test").unwrap(), + vec![ + User::new("test"), + User::new("~owner"), + User::new("&admin"), + User::new("test2"), + ] + ) + } + + #[test] + #[cfg(not(feature = "nochanlists"))] + fn user_tracking_names_part() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ + :owner!test@test PART #test\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + server.list_users("#test").unwrap(), + vec![User::new("test"), User::new("&admin")] + ) + } + + #[test] + #[cfg(not(feature = "nochanlists"))] + fn user_tracking_names_mode() { + let value = ":irc.test.net 353 test = #test :+test ~owner &admin\r\n\ + :test!test@test MODE #test +o test\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + server.list_users("#test").unwrap(), + vec![User::new("@test"), User::new("~owner"), User::new("&admin")] + ); + let mut exp = User::new("@test"); + exp.update_access_level(&Mode::Plus(ChannelMode::Voice, None)); + assert_eq!( + server.list_users("#test").unwrap()[0].highest_access_level(), + exp.highest_access_level() + ); + // The following tests if the maintained user contains the same entries as what is expected + // but ignores the ordering of these entries. + let mut levels = server.list_users("#test").unwrap()[0].access_levels(); + levels.retain(|l| exp.access_levels().contains(l)); + assert_eq!(levels.len(), exp.access_levels().len()); + } + + #[test] + #[cfg(feature = "nochanlists")] + fn no_user_tracking() { + let value = ":irc.test.net 353 test = #test :test ~owner &admin"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert!(server.list_users("#test").is_none()) + } + + #[test] + #[cfg(feature = "ctcp")] + fn finger_response() { + let value = ":test!test@test PRIVMSG test :\u{001}FINGER\u{001}\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "NOTICE test :\u{001}FINGER :test (test)\u{001}\r\n" + ); + } + + #[test] + #[cfg(feature = "ctcp")] + fn version_response() { + let value = ":test!test@test PRIVMSG test :\u{001}VERSION\u{001}\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + &format!( + "NOTICE test :\u{001}VERSION {}\u{001}\r\n", + ::VERSION_STR, + ) + ); + } + + #[test] + #[cfg(feature = "ctcp")] + fn source_response() { + let value = ":test!test@test PRIVMSG test :\u{001}SOURCE\u{001}\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "NOTICE test :\u{001}SOURCE https://github.com/aatxe/irc\u{001}\r\n\ + NOTICE test :\u{001}SOURCE\u{001}\r\n" + ); + } + + #[test] + #[cfg(feature = "ctcp")] + fn ctcp_ping_response() { + let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "NOTICE test :\u{001}PING test\u{001}\r\n" + ); + } + + #[test] + #[cfg(feature = "ctcp")] + fn time_response() { + let value = ":test!test@test PRIVMSG test :\u{001}TIME\u{001}\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + let val = get_server_value(server); + assert!(val.starts_with("NOTICE test :\u{001}TIME :")); + assert!(val.ends_with("\u{001}\r\n")); + } + + #[test] + #[cfg(feature = "ctcp")] + fn user_info_response() { + let value = ":test!test@test PRIVMSG test :\u{001}USERINFO\u{001}\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!( + &get_server_value(server)[..], + "NOTICE test :\u{001}USERINFO :Testing.\u{001}\ + \r\n" + ); + } + + #[test] + #[cfg(feature = "ctcp")] + fn ctcp_ping_no_timestamp() { + let value = ":test!test@test PRIVMSG test :\u{001}PING\u{001}\r\n"; + let server = IrcClient::from_config(Config { + mock_initial_value: Some(value.to_owned()), + ..test_config() + }).unwrap(); + server.for_each_incoming(|message| { + println!("{:?}", message); + }).unwrap(); + assert_eq!(&get_server_value(server)[..], ""); + } +} diff --git a/src/client/prelude.rs b/src/client/prelude.rs index 9361c37..6507349 100644 --- a/src/client/prelude.rs +++ b/src/client/prelude.rs @@ -23,8 +23,8 @@ pub use client::data::Config; pub use client::reactor::IrcReactor; -pub use client::server::{EachIncomingExt, IrcServer, Server}; -pub use client::server::utils::ServerExt; +pub use client::{EachIncomingExt, IrcClient, Client}; +pub use client::ext::ClientExt; pub use proto::{Capability, ChannelExt, Command, Message, NegotiationVersion, Response}; pub use proto::{ChannelMode, Mode, UserMode}; diff --git a/src/client/reactor.rs b/src/client/reactor.rs index c1ba25c..d41daaf 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -1,8 +1,8 @@ -//! A system for creating and managing IRC server connections. +//! A system for creating and managing IRC client connections. //! -//! This API provides the ability to create and manage multiple IRC servers that can run on the same +//! This API provides the ability to create and manage multiple IRC clients that can run on the same //! thread through the use of a shared event loop. It can also be used to encapsulate the dependency -//! on `tokio` and `futures` in the use of `IrcServer::new_future`. This means that knowledge of +//! on `tokio` and `futures` in the use of `IrcClient::new_future`. This means that knowledge of //! those libraries should be unnecessary for the average user. Nevertheless, this API also provides //! some escape hatches that let advanced users take further advantage of these dependencies. //! @@ -16,11 +16,11 @@ //! fn main() { //! let config = Config::default(); //! let mut reactor = IrcReactor::new().unwrap(); -//! let server = reactor.prepare_server_and_connect(&config).unwrap(); -//! reactor.register_server_with_handler(server, process_msg); +//! let client = reactor.prepare_client_and_connect(&config).unwrap(); +//! reactor.register_client_with_handler(client, process_msg); //! reactor.run().unwrap(); //! } -//! # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) } +//! # fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { Ok(()) } //! ``` use futures::{Future, IntoFuture, Stream}; @@ -28,14 +28,14 @@ use futures::future; use tokio_core::reactor::{Core, Handle}; use client::data::Config; -use client::server::{IrcServer, IrcServerFuture, PackedIrcServer, Server}; +use client::{IrcClient, IrcClientFuture, PackedIrcClient, Client}; use error; use proto::Message; /// A thin wrapper over an event loop. /// -/// An IRC reactor is used to create new connections to IRC servers and to drive the management of -/// all connected servers as the application runs. It can be used to run multiple servers on the +/// An IRC reactor is used to create new connections to IRC clients and to drive the management of +/// all connected clients as the application runs. It can be used to run multiple clients on the /// same thread, as well as to get better control over error management in an IRC client. /// /// For a full example usage, see [`irc::client::reactor`](./index.html). @@ -53,9 +53,9 @@ impl IrcReactor { }) } - /// Creates a representation of an IRC server that has not yet attempted to connect. In + /// Creates a representation of an IRC client that has not yet attempted to connect. In /// particular, this representation is as a Future that when run will produce a connected - /// [IrcServer](./server/struct.IrcServer.html). + /// [IrcClient](./client/struct.IrcClient.html). /// /// # Example /// ```no_run @@ -64,17 +64,17 @@ impl IrcReactor { /// # use irc::client::prelude::*; /// # fn main() { /// # let config = Config::default(); - /// let future_server = IrcReactor::new().and_then(|mut reactor| { - /// reactor.prepare_server(&config) + /// let future_client = IrcReactor::new().and_then(|mut reactor| { + /// reactor.prepare_client(&config) /// }); /// # } /// ``` - pub fn prepare_server<'a>(&mut self, config: &'a Config) -> error::Result> { - IrcServer::new_future(self.inner_handle(), config) + pub fn prepare_client<'a>(&mut self, config: &'a Config) -> error::Result> { + IrcClient::new_future(self.inner_handle(), config) } - /// Runs an [IrcServerFuture](./server/struct.IrcServerFuture.html), such as one from - /// `prepare_server` to completion, yielding an [IrcServer](./server/struct.IrcServer.html). + /// Runs an [IrcClientFuture](./client/struct.IrcClientFuture.html), such as one from + /// `prepare_client` to completion, yielding an [IrcClient](./client/struct.IrcClient.html). /// /// # Example /// ```no_run @@ -83,22 +83,22 @@ impl IrcReactor { /// # use irc::client::prelude::*; /// # fn main() { /// # let config = Config::default(); - /// let server = IrcReactor::new().and_then(|mut reactor| { - /// reactor.prepare_server(&config).and_then(|future| { - /// reactor.connect_server(future) + /// let client = IrcReactor::new().and_then(|mut reactor| { + /// reactor.prepare_client(&config).and_then(|future| { + /// reactor.connect_client(future) /// }) /// }); /// # } /// ``` - pub fn connect_server(&mut self, future: IrcServerFuture) -> error::Result { - self.inner.run(future).map(|PackedIrcServer(server, future)| { + pub fn connect_client(&mut self, future: IrcClientFuture) -> error::Result { + self.inner.run(future).map(|PackedIrcClient(client, future)| { self.register_future(future); - server + client }) } - /// Creates a new IRC server from the specified configuration, connecting immediately. This is - /// guaranteed to be the composition of prepare_server and connect_server. + /// Creates a new IRC client from the specified configuration, connecting immediately. This is + /// guaranteed to be the composition of prepare_client and connect_client. /// /// # Example /// ```no_run @@ -107,16 +107,16 @@ impl IrcReactor { /// # use irc::client::prelude::*; /// # fn main() { /// # let config = Config::default(); - /// let server = IrcReactor::new().and_then(|mut reactor| { - /// reactor.prepare_server_and_connect(&config) + /// let client = IrcReactor::new().and_then(|mut reactor| { + /// reactor.prepare_client_and_connect(&config) /// }); /// # } /// ``` - pub fn prepare_server_and_connect(&mut self, config: &Config) -> error::Result { - self.prepare_server(config).and_then(|future| self.connect_server(future)) + pub fn prepare_client_and_connect(&mut self, config: &Config) -> error::Result { + self.prepare_client(config).and_then(|future| self.connect_client(future)) } - /// Registers the given server with the specified message handler. The reactor will store this + /// Registers the given client with the specified message handler. The reactor will store this /// setup until the next call to run, where it will be used to process new messages over the /// connection indefinitely (or until failure). As registration is consumed by `run`, subsequent /// calls to run will require new registration. @@ -129,25 +129,25 @@ impl IrcReactor { /// # fn main() { /// # let config = Config::default(); /// let mut reactor = IrcReactor::new().unwrap(); - /// let server = reactor.prepare_server_and_connect(&config).unwrap(); - /// reactor.register_server_with_handler(server, |server, msg| { + /// let client = reactor.prepare_client_and_connect(&config).unwrap(); + /// reactor.register_client_with_handler(client, |client, msg| { /// // Message processing happens here. /// Ok(()) /// }) /// # } /// ``` - pub fn register_server_with_handler( - &mut self, server: IrcServer, handler: F - ) where F: Fn(&IrcServer, Message) -> U + 'static, + pub fn register_client_with_handler( + &mut self, client: IrcClient, handler: F + ) where F: Fn(&IrcClient, Message) -> U + 'static, U: IntoFuture + 'static { - self.handlers.push(Box::new(server.stream().for_each(move |message| { - handler(&server, message) + self.handlers.push(Box::new(client.stream().for_each(move |message| { + handler(&client, message) }))); } /// Registers an arbitrary future with this reactor. This is a sort of escape hatch that allows /// you to take more control over what runs on the reactor without requiring you to bring in - /// additional knowledge about `tokio`. It is suspected that `register_server_with_handler` will + /// additional knowledge about `tokio`. It is suspected that `register_client_with_handler` will /// be sufficient for most use cases. pub fn register_future( &mut self, future: F @@ -163,8 +163,8 @@ impl IrcReactor { } /// Consumes all registered handlers and futures, and runs them. When using - /// `register_server_with_handler`, this will block indefinitely (until failure occurs) as it - /// will simply continue to process new, incoming messages for each server that was registered. + /// `register_client_with_handler`, this will block indefinitely (until failure occurs) as it + /// will simply continue to process new, incoming messages for each client that was registered. /// /// # Example /// ```no_run @@ -175,10 +175,10 @@ impl IrcReactor { /// # fn main() { /// # let config = Config::default(); /// let mut reactor = IrcReactor::new().unwrap(); - /// let server = reactor.prepare_server_and_connect(&config).unwrap(); - /// reactor.register_server_with_handler(server, process_msg) + /// let client = reactor.prepare_client_and_connect(&config).unwrap(); + /// reactor.register_client_with_handler(client, process_msg) /// # } - /// # fn process_msg(server: &IrcServer, message: Message) -> error::Result<()> { Ok(()) } + /// # fn process_msg(client: &IrcClient, message: Message) -> error::Result<()> { Ok(()) } /// ``` pub fn run(&mut self) -> error::Result<()> { let mut handlers = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index 3077f71..fbf3cf3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,12 +3,12 @@ //! # Quick Start //! The main public API is entirely exported in [`client::prelude`](./client/prelude/index.html). This //! should include everything necessary to write an IRC client or bot. -//! +//! //! # A Whirlwind Tour //! The irc crate is divided into two main modules: [client](./client/index.html) and //! [proto](./proto/index.html). As the names suggest, the client module captures the whole of the //! client-side functionality, while the proto module features general components of an IRC protocol -//! implementation that could in principle be used in either client or server software. Both modules +//! implementation that could in principle be used in either client or client software. Both modules //! feature a number of components that are low-level and can be used to build alternative APIs for //! the IRC protocol. For the average user, the higher-level components for an IRC client are all //! re-exported in [`client::prelude`](./client/prelude/index.html). That module serves as the best @@ -22,16 +22,16 @@ //! //! # fn main() { //! // configuration is loaded from config.toml into a Config -//! let server = IrcServer::new("config.toml").unwrap(); -//! // identify comes from ServerExt -//! server.identify().unwrap(); -//! // for_each_incoming comes from Server -//! server.for_each_incoming(|irc_msg| { +//! let client = IrcClient::new("config.toml").unwrap(); +//! // identify comes from ClientExt +//! client.identify().unwrap(); +//! // for_each_incoming comes from Client +//! client.for_each_incoming(|irc_msg| { //! // irc_msg is a Message //! match irc_msg.command { -//! Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) { -//! // send_privmsg comes from ServerExt -//! server.send_privmsg(&channel, "beep boop").unwrap(); +//! Command::PRIVMSG(channel, message) => if message.contains(client.current_nickname()) { +//! // send_privmsg comes from ClientExt +//! client.send_privmsg(&channel, "beep boop").unwrap(); //! } //! _ => () //! } From 59f426ac8d1db16edbfcfce522745f01e891b960 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 03:19:05 +0100 Subject: [PATCH 30/31] Cleaned up tests and documentation after the big renaming. --- examples/multiserver.rs | 2 +- examples/reconnector.rs | 2 +- src/client/ext.rs | 174 +++++++++++----------- src/client/mod.rs | 321 +++++++++++++++++++--------------------- src/client/prelude.rs | 8 +- src/client/reactor.rs | 21 +-- src/lib.rs | 33 ++--- 7 files changed, 273 insertions(+), 288 deletions(-) diff --git a/examples/multiserver.rs b/examples/multiserver.rs index dbd2030..c88311e 100644 --- a/examples/multiserver.rs +++ b/examples/multiserver.rs @@ -24,7 +24,7 @@ fn main() { let mut reactor = IrcReactor::new().unwrap(); for config in configs { - // Immediate errors like failure to resolve the client's name or to establish any connection will + // Immediate errors like failure to resolve the server's domain or to establish any connection will // manifest here in the result of prepare_client_and_connect. let client = reactor.prepare_client_and_connect(&config).unwrap(); client.identify().unwrap(); diff --git a/examples/reconnector.rs b/examples/reconnector.rs index 35ad24e..f879f48 100644 --- a/examples/reconnector.rs +++ b/examples/reconnector.rs @@ -36,7 +36,7 @@ fn main() { }).and_then(|()| reactor.run()); match res { - // The connections ended normally (for example, they sent a QUIT message to the client). + // The connections ended normally (for example, they sent a QUIT message to the server). Ok(_) => break, // Something went wrong! We'll print the error, and restart the connections. Err(e) => eprintln!("{}", e), diff --git a/src/client/ext.rs b/src/client/ext.rs index c1bcfc0..316e433 100644 --- a/src/client/ext.rs +++ b/src/client/ext.rs @@ -31,7 +31,7 @@ //! # extern crate irc; //! # use irc::client::prelude::*; //! # fn main() { -//! # let server = IrcClient::new("config.toml").unwrap(); +//! # let server = IrcClient::new("config.toml").unwrap(); //! server.send_cap_req(&[Capability::MultiPrefix, Capability::UserhostInNames]).unwrap(); //! server.identify().unwrap(); //! # } @@ -48,7 +48,7 @@ use proto::command::Command::*; use proto::mode::ModeType; use client::Client; -/// Idiomatic extensions for sending messages to an IRC server. +/// Idiomatic extensions for sending messages to an IRC server as a [`Client`](../trait.Client.html). pub trait ClientExt: Client { /// Sends a request for a list of server capabilities for a specific IRCv3 version. fn send_cap_ls(&self, version: NegotiationVersion) -> Result<()> @@ -377,26 +377,22 @@ pub trait ClientExt: Client { } } -impl ClientExt for S -where - S: Client, -{ -} +impl ClientExt for C where C: Client {} #[cfg(test)] mod test { use super::ClientExt; use client::data::Config; use client::IrcClient; - use client::test::{get_server_value, test_config}; + use client::test::{get_client_value, test_config}; use proto::{ChannelMode, Mode}; #[test] fn identify() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.identify().unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.identify().unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "CAP END\r\nNICK :test\r\n\ USER test 0 * :test\r\n" ); @@ -404,14 +400,14 @@ mod test { #[test] fn identify_with_password() { - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { nickname: Some(format!("test")), password: Some(format!("password")), ..test_config() }).unwrap(); - server.identify().unwrap(); + client.identify().unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "CAP END\r\nPASS :password\r\nNICK :test\r\n\ USER test 0 * :test\r\n" ); @@ -419,149 +415,149 @@ mod test { #[test] fn send_pong() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_pong("irc.test.net").unwrap(); - assert_eq!(&get_server_value(server)[..], "PONG :irc.test.net\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_pong("irc.test.net").unwrap(); + assert_eq!(&get_client_value(client)[..], "PONG :irc.test.net\r\n"); } #[test] fn send_join() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_join("#test,#test2,#test3").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_join("#test,#test2,#test3").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "JOIN #test,#test2,#test3\r\n" ); } #[test] fn send_part() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_part("#test").unwrap(); - assert_eq!(&get_server_value(server)[..], "PART #test\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_part("#test").unwrap(); + assert_eq!(&get_client_value(client)[..], "PART #test\r\n"); } #[test] fn send_oper() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_oper("test", "test").unwrap(); - assert_eq!(&get_server_value(server)[..], "OPER test :test\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_oper("test", "test").unwrap(); + assert_eq!(&get_client_value(client)[..], "OPER test :test\r\n"); } #[test] fn send_privmsg() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_privmsg("#test", "Hi, everybody!").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_privmsg("#test", "Hi, everybody!").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG #test :Hi, everybody!\r\n" ); } #[test] fn send_notice() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_notice("#test", "Hi, everybody!").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_notice("#test", "Hi, everybody!").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "NOTICE #test :Hi, everybody!\r\n" ); } #[test] fn send_topic_no_topic() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_topic("#test", "").unwrap(); - assert_eq!(&get_server_value(server)[..], "TOPIC #test\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_topic("#test", "").unwrap(); + assert_eq!(&get_client_value(client)[..], "TOPIC #test\r\n"); } #[test] fn send_topic() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_topic("#test", "Testing stuff.").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_topic("#test", "Testing stuff.").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "TOPIC #test :Testing stuff.\r\n" ); } #[test] fn send_kill() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_kill("test", "Testing kills.").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_kill("test", "Testing kills.").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "KILL test :Testing kills.\r\n" ); } #[test] fn send_kick_no_message() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_kick("#test", "test", "").unwrap(); - assert_eq!(&get_server_value(server)[..], "KICK #test test\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_kick("#test", "test", "").unwrap(); + assert_eq!(&get_client_value(client)[..], "KICK #test test\r\n"); } #[test] fn send_kick() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_kick("#test", "test", "Testing kicks.").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_kick("#test", "test", "Testing kicks.").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "KICK #test test :Testing kicks.\r\n" ); } #[test] fn send_mode_no_modeparams() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap(); - assert_eq!(&get_server_value(server)[..], "MODE #test +i\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)]).unwrap(); + assert_eq!(&get_client_value(client)[..], "MODE #test +i\r\n"); } #[test] fn send_mode() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))]) + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_mode("#test", &[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))]) .unwrap(); - assert_eq!(&get_server_value(server)[..], "MODE #test +o test\r\n"); + assert_eq!(&get_client_value(client)[..], "MODE #test +o test\r\n"); } #[test] fn send_samode_no_modeparams() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_samode("#test", "+i", "").unwrap(); - assert_eq!(&get_server_value(server)[..], "SAMODE #test +i\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_samode("#test", "+i", "").unwrap(); + assert_eq!(&get_client_value(client)[..], "SAMODE #test +i\r\n"); } #[test] fn send_samode() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_samode("#test", "+o", "test").unwrap(); - assert_eq!(&get_server_value(server)[..], "SAMODE #test +o test\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_samode("#test", "+o", "test").unwrap(); + assert_eq!(&get_client_value(client)[..], "SAMODE #test +o test\r\n"); } #[test] fn send_sanick() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_sanick("test", "test2").unwrap(); - assert_eq!(&get_server_value(server)[..], "SANICK test test2\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_sanick("test", "test2").unwrap(); + assert_eq!(&get_client_value(client)[..], "SANICK test test2\r\n"); } #[test] fn send_invite() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_invite("test", "#test").unwrap(); - assert_eq!(&get_server_value(server)[..], "INVITE test #test\r\n"); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_invite("test", "#test").unwrap(); + assert_eq!(&get_client_value(client)[..], "INVITE test #test\r\n"); } #[test] #[cfg(feature = "ctcp")] fn send_ctcp() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_ctcp("test", "MESSAGE").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_ctcp("test", "MESSAGE").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG test :\u{001}MESSAGE\u{001}\r\n" ); } @@ -569,10 +565,10 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_action() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_action("test", "tests.").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_action("test", "tests.").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n" ); } @@ -580,10 +576,10 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_finger() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_finger("test").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_finger("test").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG test :\u{001}FINGER\u{001}\r\n" ); } @@ -591,10 +587,10 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_version() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_version("test").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_version("test").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG test :\u{001}VERSION\u{001}\r\n" ); } @@ -602,10 +598,10 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_source() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_source("test").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_source("test").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG test :\u{001}SOURCE\u{001}\r\n" ); } @@ -613,10 +609,10 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_user_info() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_user_info("test").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_user_info("test").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG test :\u{001}USERINFO\u{001}\r\n" ); } @@ -624,9 +620,9 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_ctcp_ping() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_ctcp_ping("test").unwrap(); - let val = get_server_value(server); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_ctcp_ping("test").unwrap(); + let val = get_client_value(client); println!("{}", val); assert!(val.starts_with("PRIVMSG test :\u{001}PING ")); assert!(val.ends_with("\u{001}\r\n")); @@ -635,10 +631,10 @@ mod test { #[test] #[cfg(feature = "ctcp")] fn send_time() { - let server = IrcClient::from_config(test_config()).unwrap(); - server.send_time("test").unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); + client.send_time("test").unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG test :\u{001}TIME\u{001}\r\n" ); } diff --git a/src/client/mod.rs b/src/client/mod.rs index fa74b18..3cd9f7d 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -2,10 +2,10 @@ //! //! This API provides the ability to connect to an IRC server via the //! [`IrcClient`](struct.IrcClient.html) type. The [`Client`](trait.Client.html) trait that -//! [`IrcClient`](struct.IrcClient.html) implements provides methods for communicating with this -//! server. An extension trait, [`ClientExt`](./utils/trait.ClientExt.html), provides short-hand for +//! [`IrcClient`](struct.IrcClient.html) implements provides methods for communicating with the +//! server. An extension trait, [`ClientExt`](./ext/trait.ClientExt.html), provides short-hand for //! sending a variety of important messages without referring to their entries in -//! [`proto::command`](../../proto/command/enum.Command.html). +//! [`proto::command`](../proto/command/enum.Command.html). //! //! # Examples //! @@ -17,13 +17,13 @@ //! use irc::client::prelude::{IrcClient, ClientExt}; //! //! # fn main() { -//! let server = IrcClient::new("config.toml").unwrap(); +//! let client = IrcClient::new("config.toml").unwrap(); //! // identify comes from `ClientExt` -//! server.identify().unwrap(); +//! client.identify().unwrap(); //! # } //! ``` //! -//! We can then use functions from [Client](trait.Client.html) to receive messages from the +//! We can then use functions from [`Client`](trait.Client.html) to receive messages from the //! server in a blocking fashion and perform any desired actions in response. The following code //! performs a simple call-and-response when the bot's name is mentioned in a channel. //! @@ -33,15 +33,14 @@ //! use irc::client::prelude::{Client, Command}; //! //! # fn main() { -//! # let server = IrcClient::new("config.toml").unwrap(); -//! # server.identify().unwrap(); -//! server.for_each_incoming(|irc_msg| { -//! match irc_msg.command { -//! Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) { -//! server.send_privmsg(&channel, "beep boop").unwrap(); +//! # let client = IrcClient::new("config.toml").unwrap(); +//! # client.identify().unwrap(); +//! client.for_each_incoming(|irc_msg| { +//! if let Command::PRIVMSG(channel, message) = irc_msg.command { +//! if message.contains(client.current_nickname()) { +//! client.send_privmsg(&channel, "beep boop").unwrap(); +//! } //! } -//! _ => () -//! } //! }).unwrap(); //! # } //! ``` @@ -91,12 +90,12 @@ pub mod transport; /// use irc::client::prelude::EachIncomingExt; /// /// # fn main() { -/// # let server = IrcClient::new("config.toml").unwrap(); -/// # server.identify().unwrap(); -/// server.stream().for_each_incoming(|irc_msg| { +/// # let client = IrcClient::new("config.toml").unwrap(); +/// # client.identify().unwrap(); +/// client.stream().for_each_incoming(|irc_msg| { /// match irc_msg.command { -/// Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) { -/// server.send_privmsg(&channel, "beep boop").unwrap(); +/// Command::PRIVMSG(channel, message) => if message.contains(client.current_nickname()) { +/// client.send_privmsg(&channel, "beep boop").unwrap(); /// } /// _ => () /// } @@ -106,10 +105,7 @@ pub mod transport; pub trait EachIncomingExt: Stream { /// Blocks on the stream, running the given function on each incoming message as they arrive. fn for_each_incoming(self, mut f: F) -> error::Result<()> - where - F: FnMut(Message) -> (), - Self: Sized, - { + where F: FnMut(Message) -> (), Self: Sized { self.for_each(|msg| { f(msg); Ok(()) @@ -121,35 +117,35 @@ impl EachIncomingExt for T where T: Stream &Config; - /// Sends a [Command](../../proto/command/enum.Command.html) to this Client. This is the core - /// primitive for sending messages to the server. In practice, it's often more pleasant (and - /// more idiomatic) to use the functions defined on [ClientExt](./utils/trait.ClientExt.html). - /// They capture a lot of the more repetitive aspects of sending messages. + /// Sends a [`Command`](../proto/command/enum.Command.html) as this `Client`. This is the + /// core primitive for sending messages to the server. In practice, it's often more pleasant + /// (and more idiomatic) to use the functions defined on + /// [`ClientExt`](./ext/trait.ClientExt.html). They capture a lot of the more repetitive + /// aspects of sending messages. /// /// # Example /// ```no_run /// # extern crate irc; /// # use irc::client::prelude::*; /// # fn main() { - /// # let server = IrcClient::new("config.toml").unwrap(); - /// server.send(Command::NICK("example".to_owned())).unwrap(); - /// server.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap(); + /// # let client = IrcClient::new("config.toml").unwrap(); + /// client.send(Command::NICK("example".to_owned())).unwrap(); + /// client.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap(); /// # } /// ``` - fn send>(&self, message: M) -> error::Result<()> - where - Self: Sized; + fn send>(&self, message: M) -> error::Result<()> where Self: Sized; - /// Gets a stream of incoming messages from the Client. This is only necessary when trying to - /// set up more complex clients, and requires use of the `futures` crate. Most IRC bots should - /// be able to get by using only `for_each_incoming` to handle received messages. You can find - /// some examples of more complex setups using `stream` in the - /// [GitHub repository](https://github.com/aatxe/irc/tree/master/examples). + /// Gets a stream of incoming messages from the `Client`'s connection. This is only necessary + /// when trying to set up more complex clients, and requires use of the `futures` crate. Most + /// IRC bots should be able to get by using only `for_each_incoming` to handle received + /// messages. You can find some examples of more complex setups using `stream` in the + /// [GitHub repository](https://github.com/aatxe/irc/tree/stable/examples). /// /// **Note**: The stream can only be returned once. Subsequent attempts will cause a panic. + // FIXME: when impl traits stabilize, we should change this return type. fn stream(&self) -> ClientStream; /// Blocks on the stream, running the given function on each incoming message as they arrive. @@ -159,22 +155,18 @@ pub trait Client { /// # extern crate irc; /// # use irc::client::prelude::{IrcClient, ClientExt, Client, Command}; /// # fn main() { - /// # let server = IrcClient::new("config.toml").unwrap(); - /// # server.identify().unwrap(); - /// server.for_each_incoming(|irc_msg| { - /// match irc_msg.command { - /// Command::PRIVMSG(channel, message) => if message.contains(server.current_nickname()) { - /// server.send_privmsg(&channel, "beep boop").unwrap(); + /// # let client = IrcClient::new("config.toml").unwrap(); + /// # client.identify().unwrap(); + /// client.for_each_incoming(|irc_msg| { + /// if let Command::PRIVMSG(channel, message) = irc_msg.command { + /// if message.contains(client.current_nickname()) { + /// client.send_privmsg(&channel, "beep boop").unwrap(); + /// } /// } - /// _ => () - /// } /// }).unwrap(); /// # } /// ``` - fn for_each_incoming(&self, f: F) -> error::Result<()> - where - F: FnMut(Message) -> (), - { + fn for_each_incoming(&self, f: F) -> error::Result<()> where F: FnMut(Message) -> () { self.stream().for_each_incoming(f) } @@ -182,7 +174,7 @@ pub trait Client { /// altogether via the `nochanlists` feature. fn list_channels(&self) -> Option>; - /// Gets a list of [Users](../data/user/struct.User.html) in the specified channel. If the + /// Gets a list of [`Users`](./data/user/struct.User.html) in the specified channel. If the /// specified channel hasn't been joined or the `nochanlists` feature is enabled, this function /// will return `None`. /// @@ -195,20 +187,20 @@ pub trait Client { /// use irc::proto::caps::Capability; /// /// # fn main() { - /// # let server = IrcClient::new("config.toml").unwrap(); - /// server.send_cap_req(&[Capability::MultiPrefix]).unwrap(); - /// server.identify().unwrap(); + /// # let client = IrcClient::new("config.toml").unwrap(); + /// client.send_cap_req(&[Capability::MultiPrefix]).unwrap(); + /// client.identify().unwrap(); /// # } /// ``` fn list_users(&self, channel: &str) -> Option>; } -/// A stream of `Messages` from the `IrcClient`. +/// A stream of `Messages` received from an IRC server via an `IrcClient`. /// /// Interaction with this stream relies on the `futures` API, but is only expected for less /// traditional use cases. To learn more, you can view the documentation for the -/// [futures](https://docs.rs/futures/) crate, or the tutorials for -/// [tokio](https://tokio.rs/docs/getting-started/futures/). +/// [`futures`](https://docs.rs/futures/) crate, or the tutorials for +/// [`tokio`](https://tokio.rs/docs/getting-started/futures/). #[derive(Debug)] pub struct ClientStream { state: Arc, @@ -250,10 +242,7 @@ impl<'a> Client for ClientState { &self.config } - fn send>(&self, msg: M) -> error::Result<()> - where - Self: Sized, - { + fn send>(&self, msg: M) -> error::Result<()> where Self: Sized { let msg = &msg.into(); self.handle_sent_message(msg)?; Ok((&self.outgoing).unbounded_send( @@ -608,13 +597,13 @@ impl ClientState { /// The canonical implementation of a connection to an IRC server. /// /// The type itself provides a number of methods to create new connections, but most of the API -/// surface is in the form of the [Client](trait.Client.html) and -/// [`ClientExt`](./utils/trait.ClientExt.html) traits that provide methods of communicating with +/// surface is in the form of the [`Client`](trait.Client.html) and +/// [`ClientExt`](./ext/trait.ClientExt.html) traits that provide methods of communicating with /// the server after connection. Cloning an `IrcClient` is relatively cheap, as it's equivalent to /// cloning a single `Arc`. This may be useful for setting up multiple threads with access to one /// connection. /// -/// For a full example usage, see [`irc::client::server`](./index.html). +/// For a full example usage, see [`irc::client`](./index.html). #[derive(Clone, Debug)] pub struct IrcClient { /// The internal, thread-safe server state. @@ -679,7 +668,7 @@ impl Client for IrcClient { } impl IrcClient { - /// Creates a new IRC Client connection from the configuration at the specified path, connecting + /// Creates a new `IrcClient` from the configuration at the specified path, connecting /// immediately. This function is short-hand for loading the configuration and then calling /// `IrcClient::from_config` and consequently inherits its behaviors. /// @@ -688,19 +677,19 @@ impl IrcClient { /// # extern crate irc; /// # use irc::client::prelude::*; /// # fn main() { - /// let server = IrcClient::new("config.toml").unwrap(); + /// let client = IrcClient::new("config.toml").unwrap(); /// # } /// ``` pub fn new>(config: P) -> error::Result { IrcClient::from_config(Config::load(config)?) } - /// Creates a new IRC server connection from the specified configuration, connecting - /// immediately. Due to current design limitations, error handling here is somewhat limited. In - /// particular, failed connections will cause the program to panic because the connection - /// attempt is made on a freshly created thread. If you need to avoid this behavior and handle - /// errors more gracefully, it is recommended that you use an - /// [IrcReactor](../reactor/struct.IrcReactor.html) instead. + /// Creates a new `IrcClient` from the specified configuration, connecting immediately. Due to + /// current design limitations, error handling here is somewhat limited. In particular, failed + /// connections will cause the program to panic because the connection attempt is made on a + /// freshly created thread. If you need to avoid this behavior and handle errors more + /// gracefully, it is recommended that you use an + /// [`IrcReactor`](./reactor/struct.IrcReactor.html) instead. /// /// # Example /// ```no_run @@ -713,7 +702,7 @@ impl IrcClient { /// server: Some("irc.example.com".to_owned()), /// .. Default::default() /// }; - /// let server = IrcClient::from_config(config).unwrap(); + /// let client = IrcClient::from_config(config).unwrap(); /// # } /// ``` pub fn from_config(config: Config) -> error::Result { @@ -751,17 +740,17 @@ impl IrcClient { }) } - /// Creates a new IRC server connection from the specified configuration and on the event loop - /// corresponding to the given handle. This can be used to set up a number of IrcClients on a + /// Creates a `Future` of an `IrcClient` from the specified configuration and on the event loop + /// corresponding to the given handle. This can be used to set up a number of `IrcClients` on a /// single, shared event loop. It can also be used to take more control over execution and error /// handling. Connection will not occur until the event loop is run. /// /// Proper usage requires familiarity with `tokio` and `futures`. You can find more information - /// in the crate documentation for [tokio-core](http://docs.rs/tokio-core) or - /// [futures](http://docs.rs/futures). Additionally, you can find detailed tutorials on using + /// in the crate documentation for [`tokio-core`](http://docs.rs/tokio-core) or + /// [`futures`](http://docs.rs/futures). Additionally, you can find detailed tutorials on using /// both libraries on the [tokio website](https://tokio.rs/docs/getting-started/tokio/). An easy /// to use abstraction that does not require this knowledge is available via - /// [`IrcReactors`](../reactor/struct.IrcReactor.html). + /// [`IrcReactors`](./reactor/struct.IrcReactor.html). /// /// # Example /// ```no_run @@ -781,11 +770,11 @@ impl IrcClient { /// let mut reactor = Core::new().unwrap(); /// let future = IrcClient::new_future(reactor.handle(), &config).unwrap(); /// // immediate connection errors (like no internet) will turn up here... - /// let PackedIrcClient(server, future) = reactor.run(future).unwrap(); + /// let PackedIrcClient(client, future) = reactor.run(future).unwrap(); /// // runtime errors (like disconnections and so forth) will turn up here... - /// reactor.run(server.stream().for_each(move |irc_msg| { + /// reactor.run(client.stream().for_each(move |irc_msg| { /// // processing messages works like usual - /// process_msg(&server, irc_msg) + /// process_msg(&client, irc_msg) /// }).join(future)).unwrap(); /// # } /// # fn process_msg(server: &IrcClient, message: Message) -> error::Result<()> { Ok(()) } @@ -820,10 +809,10 @@ impl IrcClient { /// /// Interaction with this future relies on the `futures` API, but is only expected for more advanced /// use cases. To learn more, you can view the documentation for the -/// [futures](https://docs.rs/futures/) crate, or the tutorials for -/// [tokio](https://tokio.rs/docs/getting-started/futures/). An easy to use abstraction that does -/// not require this knowledge is available via [`IrcReactors`](../reactor/struct.IrcReactor.html). - #[derive(Debug)] +/// [`futures`](https://docs.rs/futures/) crate, or the tutorials for +/// [`tokio`](https://tokio.rs/docs/getting-started/futures/). An easy to use abstraction that does +/// not require this knowledge is available via [`IrcReactors`](./reactor/struct.IrcReactor.html). +#[derive(Debug)] pub struct IrcClientFuture<'a> { conn: ConnectionFuture<'a>, _handle: Handle, @@ -858,12 +847,12 @@ impl<'a> Future for IrcClientFuture<'a> { } } -/// An `IrcClient` packaged with a future that drives its message sending. In order for the server +/// An `IrcClient` packaged with a future that drives its message sending. In order for the client /// to actually work properly, this future _must_ be running. /// /// This type should only be used by advanced users who are familiar with the implementation of this /// crate. An easy to use abstraction that does not require this knowledge is available via -/// [`IrcReactors`](../reactor/struct.IrcReactor.html). +/// [`IrcReactors`](./reactor/struct.IrcReactor.html). pub struct PackedIrcClient(pub IrcClient, pub Box>); #[cfg(test)] @@ -893,11 +882,11 @@ mod test { } } - pub fn get_server_value(server: IrcClient) -> String { + pub fn get_client_value(client: IrcClient) -> String { // We sleep here because of synchronization issues. // We can't guarantee that everything will have been sent by the time of this call. thread::sleep(Duration::from_millis(100)); - server.log_view().sent().unwrap().iter().fold(String::new(), |mut acc, msg| { + client.log_view().sent().unwrap().iter().fold(String::new(), |mut acc, msg| { acc.push_str(&msg.to_string()); acc }) @@ -907,12 +896,12 @@ mod test { fn stream() { let exp = "PRIVMSG test :Hi!\r\nPRIVMSG test :This is a test!\r\n\ :test!test@test JOIN #test\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(exp.to_owned()), ..test_config() }).unwrap(); let mut messages = String::new(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { messages.push_str(&message.to_string()); }).unwrap(); assert_eq!(&messages[..], exp); @@ -921,15 +910,15 @@ mod test { #[test] fn handle_message() { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "JOIN #test\r\nJOIN #test2\r\n" ); } @@ -937,17 +926,17 @@ mod test { #[test] fn handle_end_motd_with_nick_password() { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), nick_password: Some(format!("password")), channels: Some(vec![format!("#test"), format!("#test2")]), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "NICKSERV IDENTIFY password\r\nJOIN #test\r\n\ JOIN #test2\r\n" ); @@ -956,7 +945,7 @@ mod test { #[test] fn handle_end_motd_with_chan_keys() { let value = ":irc.test.net 376 test :End of /MOTD command\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some(format!("test")), channels: Some(vec![format!("#test"), format!("#test2")]), @@ -967,11 +956,11 @@ mod test { }, ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "JOIN #test\r\nJOIN #test2 password\r\n" ); } @@ -980,7 +969,7 @@ mod test { fn handle_end_motd_with_ghost() { let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\ :irc.test.net 376 test2 :End of /MOTD command.\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some(format!("test")), alt_nicks: Some(vec![format!("test2")]), @@ -989,11 +978,11 @@ mod test { should_ghost: Some(true), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "NICK :test2\r\nNICKSERV GHOST test password\r\n\ NICK :test\r\nNICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n" ); @@ -1003,7 +992,7 @@ mod test { fn handle_end_motd_with_ghost_seq() { let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\ :irc.test.net 376 test2 :End of /MOTD command.\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some(format!("test")), alt_nicks: Some(vec![format!("test2")]), @@ -1013,11 +1002,11 @@ mod test { ghost_sequence: Some(vec![format!("RECOVER"), format!("RELEASE")]), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "NICK :test2\r\nNICKSERV RECOVER test password\ \r\nNICKSERV RELEASE test password\r\nNICK :test\r\nNICKSERV IDENTIFY password\ \r\nJOIN #test\r\nJOIN #test2\r\n" @@ -1027,18 +1016,18 @@ mod test { #[test] fn handle_end_motd_with_umodes() { let value = ":irc.test.net 376 test :End of /MOTD command.\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), nickname: Some(format!("test")), umodes: Some(format!("+B")), channels: Some(vec![format!("#test"), format!("#test2")]), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "MODE test +B\r\nJOIN #test\r\nJOIN #test2\r\n" ); } @@ -1046,14 +1035,14 @@ mod test { #[test] fn nickname_in_use() { let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); - assert_eq!(&get_server_value(server)[..], "NICK :test2\r\n"); + assert_eq!(&get_client_value(client)[..], "NICK :test2\r\n"); } #[test] @@ -1061,82 +1050,82 @@ mod test { fn ran_out_of_nicknames() { let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\ :irc.pdgn.co 433 * test2 :Nickname is already in use.\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); } #[test] fn send() { - let server = IrcClient::from_config(test_config()).unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); assert!( - server + client .send(PRIVMSG(format!("#test"), format!("Hi there!"))) .is_ok() ); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "PRIVMSG #test :Hi there!\r\n" ); } #[test] fn send_no_newline_injection() { - let server = IrcClient::from_config(test_config()).unwrap(); + let client = IrcClient::from_config(test_config()).unwrap(); assert!( - server + client .send(PRIVMSG(format!("#test"), format!("Hi there!\r\nJOIN #bad"))) .is_ok() ); - assert_eq!(&get_server_value(server)[..], "PRIVMSG #test :Hi there!\r\n"); + assert_eq!(&get_client_value(client)[..], "PRIVMSG #test :Hi there!\r\n"); } #[test] #[cfg(not(feature = "nochanlists"))] fn channel_tracking_names() { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); - assert_eq!(server.list_channels().unwrap(), vec!["#test".to_owned()]) + assert_eq!(client.list_channels().unwrap(), vec!["#test".to_owned()]) } #[test] #[cfg(not(feature = "nochanlists"))] fn channel_tracking_names_part() { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); - assert!(server.send(PART(format!("#test"), None)).is_ok()); - assert!(server.list_channels().unwrap().is_empty()) + assert!(client.send(PART(format!("#test"), None)).is_ok()); + assert!(client.list_channels().unwrap().is_empty()) } #[test] #[cfg(not(feature = "nochanlists"))] fn user_tracking_names() { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - server.list_users("#test").unwrap(), + client.list_users("#test").unwrap(), vec![User::new("test"), User::new("~owner"), User::new("&admin")] ) } @@ -1146,15 +1135,15 @@ mod test { fn user_tracking_names_join() { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ :test2!test@test JOIN #test\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - server.list_users("#test").unwrap(), + client.list_users("#test").unwrap(), vec![ User::new("test"), User::new("~owner"), @@ -1169,15 +1158,15 @@ mod test { fn user_tracking_names_part() { let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\ :owner!test@test PART #test\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - server.list_users("#test").unwrap(), + client.list_users("#test").unwrap(), vec![User::new("test"), User::new("&admin")] ) } @@ -1187,26 +1176,26 @@ mod test { fn user_tracking_names_mode() { let value = ":irc.test.net 353 test = #test :+test ~owner &admin\r\n\ :test!test@test MODE #test +o test\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - server.list_users("#test").unwrap(), + client.list_users("#test").unwrap(), vec![User::new("@test"), User::new("~owner"), User::new("&admin")] ); let mut exp = User::new("@test"); exp.update_access_level(&Mode::Plus(ChannelMode::Voice, None)); assert_eq!( - server.list_users("#test").unwrap()[0].highest_access_level(), + client.list_users("#test").unwrap()[0].highest_access_level(), exp.highest_access_level() ); // The following tests if the maintained user contains the same entries as what is expected // but ignores the ordering of these entries. - let mut levels = server.list_users("#test").unwrap()[0].access_levels(); + let mut levels = client.list_users("#test").unwrap()[0].access_levels(); levels.retain(|l| exp.access_levels().contains(l)); assert_eq!(levels.len(), exp.access_levels().len()); } @@ -1215,29 +1204,29 @@ mod test { #[cfg(feature = "nochanlists")] fn no_user_tracking() { let value = ":irc.test.net 353 test = #test :test ~owner &admin"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); - assert!(server.list_users("#test").is_none()) + assert!(client.list_users("#test").is_none()) } #[test] #[cfg(feature = "ctcp")] fn finger_response() { let value = ":test!test@test PRIVMSG test :\u{001}FINGER\u{001}\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "NOTICE test :\u{001}FINGER :test (test)\u{001}\r\n" ); } @@ -1246,15 +1235,15 @@ mod test { #[cfg(feature = "ctcp")] fn version_response() { let value = ":test!test@test PRIVMSG test :\u{001}VERSION\u{001}\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], &format!( "NOTICE test :\u{001}VERSION {}\u{001}\r\n", ::VERSION_STR, @@ -1266,15 +1255,15 @@ mod test { #[cfg(feature = "ctcp")] fn source_response() { let value = ":test!test@test PRIVMSG test :\u{001}SOURCE\u{001}\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "NOTICE test :\u{001}SOURCE https://github.com/aatxe/irc\u{001}\r\n\ NOTICE test :\u{001}SOURCE\u{001}\r\n" ); @@ -1284,15 +1273,15 @@ mod test { #[cfg(feature = "ctcp")] fn ctcp_ping_response() { let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "NOTICE test :\u{001}PING test\u{001}\r\n" ); } @@ -1301,14 +1290,14 @@ mod test { #[cfg(feature = "ctcp")] fn time_response() { let value = ":test!test@test PRIVMSG test :\u{001}TIME\u{001}\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); - let val = get_server_value(server); + let val = get_client_value(client); assert!(val.starts_with("NOTICE test :\u{001}TIME :")); assert!(val.ends_with("\u{001}\r\n")); } @@ -1317,15 +1306,15 @@ mod test { #[cfg(feature = "ctcp")] fn user_info_response() { let value = ":test!test@test PRIVMSG test :\u{001}USERINFO\u{001}\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); assert_eq!( - &get_server_value(server)[..], + &get_client_value(client)[..], "NOTICE test :\u{001}USERINFO :Testing.\u{001}\ \r\n" ); @@ -1335,13 +1324,13 @@ mod test { #[cfg(feature = "ctcp")] fn ctcp_ping_no_timestamp() { let value = ":test!test@test PRIVMSG test :\u{001}PING\u{001}\r\n"; - let server = IrcClient::from_config(Config { + let client = IrcClient::from_config(Config { mock_initial_value: Some(value.to_owned()), ..test_config() }).unwrap(); - server.for_each_incoming(|message| { + client.for_each_incoming(|message| { println!("{:?}", message); }).unwrap(); - assert_eq!(&get_server_value(server)[..], ""); + assert_eq!(&get_client_value(client)[..], ""); } } diff --git a/src/client/prelude.rs b/src/client/prelude.rs index 6507349..ef92e0c 100644 --- a/src/client/prelude.rs +++ b/src/client/prelude.rs @@ -1,15 +1,15 @@ //! A client-side IRC prelude, re-exporting the complete high-level IRC client API. //! //! # Structure -//! A connection to an IRC server is represented by an `IrcServer` which is configured using a +//! A connection to an IRC server is created via an `IrcClient` which is configured using a //! `Config` struct that defines data such as which server to connect to, on what port, and -//! using what nickname. The `Server` trait provides an API for actually interacting with the +//! using what nickname. The `Client` trait provides an API for actually interacting with the //! server once a connection has been established. This API intentionally offers only a single //! method to send `Commands` because it makes it easy to see the whole set of possible -//! interactions with a server. The `ServerExt` trait addresses this deficiency by defining a +//! interactions with a server. The `ClientExt` trait addresses this deficiency by defining a //! number of methods that provide a more clear and succinct interface for sending various //! common IRC commands to the server. An `IrcReactor` can be used to create and manage multiple -//! `IrcServers` with more fine-grained control over error management. +//! `IrcClients` with more fine-grained control over error management. //! //! The various `proto` types capture details of the IRC protocol that are used throughout the //! client API. `Message`, `Command`, and `Response` are used to send and receive messages along diff --git a/src/client/reactor.rs b/src/client/reactor.rs index d41daaf..3149bc1 100644 --- a/src/client/reactor.rs +++ b/src/client/reactor.rs @@ -5,7 +5,7 @@ //! on `tokio` and `futures` in the use of `IrcClient::new_future`. This means that knowledge of //! those libraries should be unnecessary for the average user. Nevertheless, this API also provides //! some escape hatches that let advanced users take further advantage of these dependencies. -//! +//! //! # Example //! ```no_run //! # extern crate irc; @@ -34,9 +34,9 @@ use proto::Message; /// A thin wrapper over an event loop. /// -/// An IRC reactor is used to create new connections to IRC clients and to drive the management of -/// all connected clients as the application runs. It can be used to run multiple clients on the -/// same thread, as well as to get better control over error management in an IRC client. +/// An IRC reactor is used to create new IRC clients and to drive the management of all connected +/// clients as the application runs. It can be used to run multiple clients on the same thread, as +/// well as to get better control over error management in an IRC client. /// /// For a full example usage, see [`irc::client::reactor`](./index.html). pub struct IrcReactor { @@ -54,8 +54,8 @@ impl IrcReactor { } /// Creates a representation of an IRC client that has not yet attempted to connect. In - /// particular, this representation is as a Future that when run will produce a connected - /// [IrcClient](./client/struct.IrcClient.html). + /// particular, this representation is as a `Future` that when run will produce a connected + /// [`IrcClient`](../struct.IrcClient.html). /// /// # Example /// ```no_run @@ -73,8 +73,8 @@ impl IrcReactor { IrcClient::new_future(self.inner_handle(), config) } - /// Runs an [IrcClientFuture](./client/struct.IrcClientFuture.html), such as one from - /// `prepare_client` to completion, yielding an [IrcClient](./client/struct.IrcClient.html). + /// Runs an [`IrcClientFuture`](../struct.IrcClientFuture.html), such as one from + /// `prepare_client` to completion, yielding an [`IrcClient`](../struct.IrcClient.html). /// /// # Example /// ```no_run @@ -97,8 +97,9 @@ impl IrcReactor { }) } - /// Creates a new IRC client from the specified configuration, connecting immediately. This is - /// guaranteed to be the composition of prepare_client and connect_client. + /// Creates a new [`IrcClient`](../struct.IrcClient.html) from the specified configuration, + /// connecting immediately. This is guaranteed to be the composition of `prepare_client` and + /// `connect_client`. /// /// # Example /// ```no_run diff --git a/src/lib.rs b/src/lib.rs index fbf3cf3..3c9d32d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,18 @@ //! A simple, thread-safe, and async-friendly library for IRC clients. //! //! # Quick Start -//! The main public API is entirely exported in [`client::prelude`](./client/prelude/index.html). This -//! should include everything necessary to write an IRC client or bot. +//! The main public API is entirely exported in [`client::prelude`](./client/prelude/index.html). +//! This should include everything necessary to write an IRC client or bot. //! //! # A Whirlwind Tour -//! The irc crate is divided into two main modules: [client](./client/index.html) and -//! [proto](./proto/index.html). As the names suggest, the client module captures the whole of the -//! client-side functionality, while the proto module features general components of an IRC protocol -//! implementation that could in principle be used in either client or client software. Both modules -//! feature a number of components that are low-level and can be used to build alternative APIs for -//! the IRC protocol. For the average user, the higher-level components for an IRC client are all -//! re-exported in [`client::prelude`](./client/prelude/index.html). That module serves as the best -//! starting point for a new user trying to understand the high-level API. +//! The irc crate is divided into two main modules: [`client`](./client/index.html) and +//! [`proto`](./proto/index.html). As the names suggest, the `client` module captures the whole of +//! the client-side functionality, while the `proto` module features general components of an IRC +//! protocol implementation that could in principle be used in either client or server software. +//! Both modules feature a number of components that are low-level and can be used to build +//! alternative APIs for the IRC protocol. For the average user, the higher-level components for an +//! IRC client are all re-exported in [`client::prelude`](./client/prelude/index.html). That module +//! serves as the best starting point for a new user trying to understand the high-level API. //! //! # Example //! @@ -27,14 +27,13 @@ //! client.identify().unwrap(); //! // for_each_incoming comes from Client //! client.for_each_incoming(|irc_msg| { -//! // irc_msg is a Message -//! match irc_msg.command { -//! Command::PRIVMSG(channel, message) => if message.contains(client.current_nickname()) { -//! // send_privmsg comes from ClientExt -//! client.send_privmsg(&channel, "beep boop").unwrap(); +//! // irc_msg is a Message +//! if let Command::PRIVMSG(channel, message) = irc_msg.command { +//! if message.contains(client.current_nickname()) { +//! // send_privmsg comes from ClientExt +//! client.send_privmsg(&channel, "beep boop").unwrap(); +//! } //! } -//! _ => () -//! } //! }).unwrap(); //! # } //! ``` From c694367b57d5099887c9f9a8e469da88a59b5992 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Sun, 28 Jan 2018 04:27:08 +0100 Subject: [PATCH 31/31] Bumped version number to 0.13. --- Cargo.toml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d69270..ffb867d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "irc" -version = "0.12.8" +version = "0.13.0" description = "A simple, thread-safe, and async-friendly library for IRC clients." authors = ["Aaron Weiss "] license = "MPL-2.0" diff --git a/README.md b/README.md index 1fd7117..77cf774 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ can be disabled accordingly. ## Getting Started ## -To start using this library with cargo, you can simply add `irc = "0.12"` to your dependencies in +To start using this library with cargo, you can simply add `irc = "0.13"` to your dependencies in your Cargo.toml file. You'll likely want to take a look at some of the examples, as well as the documentation. You'll also be able to find below a small template to get a feel for the library.