//! A simple IRC crate written in rust //! ```no_run //! use circe::{commands::Command, Client, Config}; //! fn main() -> Result<(), std::io::Error> { //! let config = Default::default(); //! let mut client = Client::new(config)?; //! client.identify()?; //! //! loop { //! if let Ok(ref command) = client.read() { //! if let Command::OTHER(line) = command { //! print!("{}", line); //! } //! if let Command::PRIVMSG(nick, channel, message) = command { //! println!("PRIVMSG received from {}: {} {}", nick, channel, message); //! } //! } //! # break; //! } //! # Ok(()) //! } #![warn(missing_docs)] #![allow(clippy::too_many_lines)] #[cfg(feature = "tls")] use native_tls::TlsConnector; use std::borrow::Cow; use std::fs::File; use std::io::{Error, Read, Write}; use std::net::TcpStream; use std::path::Path; use serde_derive::Deserialize; /// IRC comamnds pub mod commands; /// An IRC client pub struct Client { config: Config, #[cfg(not(feature = "tls"))] stream: TcpStream, #[cfg(feature = "tls")] stream: native_tls::TlsStream, } /// Config for the IRC client #[derive(Clone, Deserialize, Default)] pub struct Config { channels: Vec, host: String, mode: Option, nickname: Option, port: u16, username: String, } /// Custom Error for the `read` function #[derive(Debug)] pub struct NoNewLines; impl std::fmt::Display for NoNewLines { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Now new lines from the stream.") } } impl std::error::Error for NoNewLines {} impl Client { /// Creates a new client with a given [`Config`]. /// ```no_run /// # use circe::*; /// # let config = Default::default(); /// let mut client = Client::new(config)?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not connect to the host. /// # Panics /// Panics if the client can't connect to the given host. pub fn new(config: Config) -> Result { let stream = TcpStream::connect(format!("{}:{}", config.host, config.port)).unwrap(); #[cfg(feature = "tls")] let sslstream: native_tls::TlsStream; #[cfg(feature = "tls")] { let connector = TlsConnector::new().unwrap(); sslstream = connector.connect(config.host.as_str(), stream).unwrap(); }; #[cfg(feature = "tls")] return Ok(Self { config, stream: sslstream, }); #[cfg(not(feature = "tls"))] return Ok(Self { config, stream }); } /// Identify user and joins the in the [`Config`] specified channels. /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.identify()?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn identify(&mut self) -> Result<(), Error> { self.write_command(commands::Command::CAP(commands::CapMode::LS))?; self.write_command(commands::Command::CAP(commands::CapMode::END))?; self.write_command(commands::Command::USER( self.config.username.clone(), "*".into(), "*".into(), self.config.username.clone(), ))?; if let Some(nick) = self.config.nickname.clone() { self.write_command(commands::Command::NICK(nick))?; } else { self.write_command(commands::Command::NICK(self.config.username.clone()))?; } loop { if let Ok(ref command) = self.read() { match command { commands::Command::PING(code) => { self.write_command(commands::Command::PONG(code.to_string()))?; } commands::Command::OTHER(line) => { #[cfg(feature = "debug")] println!("{}", line); if line.contains("001") { break; } } _ => {} } } } let config = self.config.clone(); self.write_command(commands::Command::MODE(config.username, config.mode))?; for channel in &config.channels { self.write_command(commands::Command::JOIN(channel.to_string()))?; } Ok(()) } fn read_string(&mut self) -> Option { let mut buffer = [0u8; 512]; match self.stream.read(&mut buffer) { Ok(_) => {} Err(_) => return None, }; let res = String::from_utf8_lossy(&buffer); // The trimming is required because if the message is less than 512 bytes it will be // padded with a bunch of 0u8 because of the pre-allocated buffer Some(res.trim().trim_matches(char::from(0)).trim().into()) } /// Read data coming from the IRC as a [`commands::Command`]. /// ```no_run /// # use circe::*; /// # use circe::commands::Command; /// # fn main() -> Result<(), std::io::Error> { /// # let config = Default::default(); /// # let mut client = Client::new(config)?; /// if let Ok(ref command) = client.read() { /// if let Command::OTHER(line) = command { /// print!("{}", line); /// } /// } /// # Ok::<(), std::io::Error>(()) /// # } /// ``` /// # Errors /// Returns error if there are no new messages. This should not be taken as an actual error, because nothing went wrong. pub fn read(&mut self) -> Result { if let Some(string) = self.read_string() { #[cfg(feature = "debug")] println!("{:#?}", string); let command = commands::Command::command_from_str(&string); if let commands::Command::PONG(command) = command { if let Err(_e) = self.write_command(commands::Command::PONG(command)) { return Err(NoNewLines); } return Ok(commands::Command::PONG("".to_string())); } return Ok(command); } Err(NoNewLines) } fn write(&mut self, data: &str) -> Result<(), Error> { let formatted = { let new = format!("{}\r\n", data); Cow::Owned(new) as Cow }; #[cfg(feature = "debug")] println!("{:?}", formatted); self.stream.write_all(formatted.as_bytes())?; Ok(()) } /// Send a [`commands::Command`] to the IRC.
/// Not reccomended to use, use the helper functions instead. /// ```no_run /// # use circe::*; /// # use circe::commands::Command; /// # let mut client = Client::new(Default::default())?; /// client.write_command(Command::PRIVMSG("".to_string(), "#main".to_string(), "Hello".to_string()))?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn write_command(&mut self, command: commands::Command) -> Result<(), Error> { use commands::Command::{ ADMIN, AWAY, CAP, INVITE, JOIN, LIST, MODE, NAMES, NICK, OPER, OTHER, PART, PASS, PING, PONG, PRIVMSG, QUIT, TOPIC, USER, }; let computed = match command { ADMIN(target) => { let formatted = format!("ADMIN {}", target); Cow::Owned(formatted) as Cow } AWAY(message) => { let formatted = format!("AWAY {}", message); Cow::Owned(formatted) as Cow } CAP(mode) => { use commands::CapMode::{END, LS}; Cow::Borrowed(match mode { LS => "CAP LS 302", END => "CAP END", }) as Cow } INVITE(username, channel) => { let formatted = format!("INVITE {} {}", username, channel); Cow::Owned(formatted) as Cow } JOIN(channel) => { let formatted = format!("JOIN {}", channel); Cow::Owned(formatted) as Cow } LIST(channel, server) => { let mut formatted = "LIST".to_string(); if let Some(channel) = channel { formatted.push_str(format!(" {}", channel).as_str()); } if let Some(server) = server { formatted.push_str(format!(" {}", server).as_str()); } Cow::Owned(formatted) as Cow } NAMES(channel, server) => { let formatted = { if let Some(server) = server { format!("NAMES {} {}", channel, server) } else { format!("NAMES {}", channel) } }; Cow::Owned(formatted) as Cow } NICK(nickname) => { let formatted = format!("NICK {}", nickname); Cow::Owned(formatted) as Cow } MODE(target, mode) => { let formatted = { if let Some(mode) = mode { format!("MODE {} {}", target, mode) } else { format!("MODE {}", target) } }; Cow::Owned(formatted) as Cow } OPER(nick, password) => { let formatted = format!("OPER {} {}", nick, password); Cow::Owned(formatted) as Cow } OTHER(_) => { return Err(Error::new( std::io::ErrorKind::Other, "Cannot write commands of type OTHER", )); } PART(target) => { let formatted = format!("PART {}", target); Cow::Owned(formatted) as Cow } PASS(password) => { let formatted = format!("PASS {}", password); Cow::Owned(formatted) as Cow } PING(target) => { let formatted = format!("PING {}", target); Cow::Owned(formatted) as Cow } PONG(code) => { let formatted = format!("PONG {}", code); Cow::Owned(formatted) as Cow } PRIVMSG(_, target, message) => { let formatted = format!("PRIVMSG {} {}", target, message); Cow::Owned(formatted) as Cow } QUIT(message) => { let formatted = format!("QUIT :{}", message); Cow::Owned(formatted) as Cow } TOPIC(channel, topic) => { let formatted = { if let Some(topic) = topic { format!("TOPIC {} :{}", channel, topic) } else { format!("TOPIC {}", channel) } }; Cow::Owned(formatted) as Cow } USER(username, s1, s2, realname) => { let formatted = format!("USER {} {} {} :{}", username, s1, s2, realname); Cow::Owned(formatted) as Cow } }; self.write(&computed)?; Ok(()) } // Helper commands /// Helper function for requesting information about the ADMIN of an IRC server /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.admin("192.168.178.100")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn admin(&mut self, target: &str) -> Result<(), Error> { self.write_command(commands::Command::ADMIN(target.to_string()))?; Ok(()) } /// Helper function for setting the users status to AWAY /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.away("AFK")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn away(&mut self, message: &str) -> Result<(), Error> { self.write_command(commands::Command::AWAY(message.to_string()))?; Ok(()) } /// Helper function for sending PRIVMSGs. /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.privmsg("#main", "Hello")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn privmsg(&mut self, channel: &str, message: &str) -> Result<(), Error> { self.write_command(commands::Command::PRIVMSG( String::from(""), channel.to_string(), message.to_string(), ))?; Ok(()) } /// Helper function to INVITE people to a channels /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.invite("liblirc", "#circe")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn invite(&mut self, username: &str, channel: &str) -> Result<(), Error> { self.write_command(commands::Command::INVITE( username.to_string(), channel.to_string(), ))?; Ok(()) } /// Helper function for sending JOINs. /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.join("#main")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn join(&mut self, channel: &str) -> Result<(), Error> { self.write_command(commands::Command::JOIN(channel.to_string()))?; Ok(()) } /// Helper function for ``LISTing`` channels and modes /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.list(None, None)?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn list(&mut self, channel: Option<&str>, server: Option<&str>) -> Result<(), Error> { let channel_config = channel.map(std::string::ToString::to_string); let server_config = server.map(std::string::ToString::to_string); self.write_command(commands::Command::LIST(channel_config, server_config))?; Ok(()) } /// Helper function for getting all nicknames in a channel /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.names("#main,#circe", None)?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn names(&mut self, channel: &str, server: Option<&str>) -> Result<(), Error> { if let Some(server) = server { self.write_command(commands::Command::NAMES( channel.to_string(), Some(server.to_string()), ))?; } else { self.write_command(commands::Command::NAMES(channel.to_string(), None))?; } Ok(()) } /// Helper function to try to register as a channel operator /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.oper("username", "password")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn oper(&mut self, username: &str, password: &str) -> Result<(), Error> { self.write_command(commands::Command::OPER( username.to_string(), password.to_string(), ))?; Ok(()) } /// Helper function for sending MODEs. /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.mode("test", Some("+B"))?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn mode(&mut self, target: &str, mode: Option<&str>) -> Result<(), Error> { if let Some(mode) = mode { self.write_command(commands::Command::MODE( target.to_string(), Some(mode.to_string()), ))?; } else { self.write_command(commands::Command::MODE(target.to_string(), None))?; } Ok(()) } /// Helper function for leaving channels. /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.part("#main")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn part(&mut self, target: &str) -> Result<(), Error> { self.write_command(commands::Command::PART(target.to_string()))?; Ok(()) } /// Helper function for setting or getting the topic of a channel /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.topic("#main", Some("main channel"))?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn topic(&mut self, channel: &str, topic: Option<&str>) -> Result<(), Error> { if let Some(topic) = topic { self.write_command(commands::Command::TOPIC( channel.to_string(), Some(topic.to_string()), ))?; } else { self.write_command(commands::Command::TOPIC(channel.to_string(), None))?; } Ok(()) } /// Helper function for leaving the IRC server and shutting down the TCP stream afterwards. /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Default::default())?; /// client.quit(None)?; /// # Ok::<(), std::io::Error>(()) /// ``` /// # Errors /// Returns error if the client could not write to the stream. pub fn quit(&mut self, message: Option<&str>) -> Result<(), Error> { if let Some(message) = message { self.write_command(commands::Command::QUIT(message.to_string()))?; } else { self.write_command(commands::Command::QUIT(format!( "circe {} (https://crates.io/crates/circe)", env!("CARGO_PKG_VERSION") )))?; } #[cfg(not(feature = "tls"))] self.stream.shutdown(std::net::Shutdown::Both)?; #[cfg(feature = "tls")] self.stream.shutdown()?; Ok(()) } } impl Config { /// Create a new config for the client /// /// ```rust /// # use circe::*; /// let config = Config::new( /// &["#main", "#circe"], /// "192.168.178.100", /// Some("+B"), /// Some("circe"), /// 6667, /// "circe", /// ); /// ``` #[must_use] pub fn new( channels: &[&'static str], host: &str, mode: Option<&'static str>, nickname: Option<&'static str>, port: u16, username: &str, ) -> Self { // Conversion from &'static str to String let channels_config = channels .iter() .map(|channel| (*channel).to_string()) .collect(); let mode_config: Option; if let Some(mode) = mode { mode_config = Some(mode.to_string()); } else { mode_config = None; } let nickname_config: Option; if let Some(nickname) = nickname { nickname_config = Some(nickname.to_string()); } else { nickname_config = Some(username.to_string()); } Self { channels: channels_config, host: host.into(), mode: mode_config, nickname: nickname_config, port, username: username.into(), } } /// _This feature requires the `toml_support` feature to be enabled._ /// /// Create a config from a toml file /// ```toml /// channels = ["#main", "#main2"] /// host = "192.168.178.100" /// mode = "+B" /// nickname = "circe" /// port = 6667 /// username = "circe" /// ``` /// # Errors /// Returns an Error if the file cannot be opened or if the TOML is invalid /// #[cfg(feature = "toml_support")] pub fn from_toml>(path: P) -> Result { let mut file = File::open(&path)?; let mut data = String::new(); file.read_to_string(&mut data)?; toml::from_str(&data).map_err(|e| { use std::io::ErrorKind; Error::new(ErrorKind::Other, format!("Invalid TOML: {}", e)) }) } }