//! A simple IRC crate written in rust //! ```no_run //! use circe::*; //! fn main() -> Result<(), std::io::Error> { //! let config = Config::from_toml("config.toml")?; //! 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(channel, message) = command { //! println!("PRIVMSG received: {} {}", channel, message); //! } //! } //! # break; //! } //! //! # Ok(()) //! } #![warn(missing_docs)] 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; /// An IRC client pub struct Client { config: Config, stream: TcpStream, } /// Config for the IRC client #[derive(Clone, Deserialize, Default)] pub struct Config { channels: Box<[String]>, host: String, mode: Option, nickname: Option, port: u16, username: String, } #[doc(hidden)] #[derive(Debug)] pub enum CapMode { LS, END, } /// IRC commands #[derive(Debug)] pub enum Command { #[doc(hidden)] CAP(CapMode), /// Joins a channel /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.write_command(Command::JOIN("#main".to_string()))?; /// # Ok::<(), std::io::Error>(()) /// ``` JOIN( /// Channel String, ), /// Sets the mode of the user /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.write_command(Command::MODE("#main".to_string(), Some("+B".to_string())))?; /// # Ok::<(), std::io::Error>(()) /// ``` /// If the MODE is not given (e.g. None), then the client will send "MODE target" MODE( /// Channel String, /// Mode Option, ), #[doc(hidden)] NICK(String), /// Everything that is not a command OTHER(String), /// Ping another user or the server PING( /// target String, ), #[doc(hidden)] PONG(String), /// Sends a message in a channel /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.write_command(Command::PRIVMSG("#main".to_string(), "This is an example message".to_string()))?; /// # Ok::<(), std::io::Error>(()) /// ``` PRIVMSG( /// Channel String, /// Message String, ), #[doc(hidden)] USER(String, String, String, String), } impl Command { fn from_str(s: &str) -> Self { let new = s.trim(); if new.starts_with("PING") { let command: String = String::from(new.split_whitespace().collect::>()[1]); return Self::PING(command); } else if new.contains("PRIVMSG") { let parts: Vec<&str> = new.split_whitespace().collect(); let target = parts[2]; let mut builder = String::new(); for part in parts[3..].to_vec() { builder.push_str(&format!("{} ", part)); } return Self::PRIVMSG(target.to_string(), (&builder[1..]).to_string()); } Self::OTHER(new.to_string()) } } impl Client { /// Creates a new client with a given config /// ```no_run /// # use circe::*; /// # let config = Config::from_toml("config.toml")?; /// let mut client = Client::new(config)?; /// # Ok::<(), std::io::Error>(()) /// ``` /// /// Errors if the client could not connect to the given host. pub fn new(config: Config) -> Result { let stream = TcpStream::connect(format!("{}:{}", config.host, config.port))?; Ok(Self { stream, config }) } /// Identify user and join the specified channels /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.identify()?; /// # Ok::<(), std::io::Error>(()) /// ``` /// /// Errors if the client could not write to the stream. pub fn identify(&mut self) -> Result<(), Error> { self.write_command(Command::CAP(CapMode::END))?; self.write_command(Command::USER( self.config.username.clone(), "*".into(), "*".into(), self.config.username.clone(), ))?; if let Some(nick) = self.config.nickname.clone() { self.write_command(Command::NICK(nick))?; } else { self.write_command(Command::NICK(self.config.username.clone()))?; } loop { if let Ok(ref command) = self.read() { match command { Command::PING(code) => self.write_command(Command::PONG(code.to_string()))?, Command::OTHER(line) => { if line.contains("001") { break; } } _ => {} } } } let config = self.config.clone(); self.write_command(Command::MODE(config.username, config.mode))?; for channel in config.channels.iter() { self.write_command(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, }; Some(String::from_utf8_lossy(&buffer).into()) } /// Read data coming from the IRC as a [`Command`] /// ```no_run /// # use circe::*; /// # fn main() -> Result<(), std::io::Error> { /// # let config = Config::from_toml("config.toml")?; /// # 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 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() { return Ok(Command::from_str(&string)); } Err(()) } fn write(&mut self, data: &str) -> Result<(), Error> { let formatted = { let new = format!("{}\r\n", data); Cow::Owned(new) as Cow }; self.stream.write(formatted.as_bytes())?; Ok(()) } /// Send a [`Command`] to the IRC /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.write_command(Command::PRIVMSG("#main".to_string(), "Hello".to_string()))?; /// # Ok::<(), std::io::Error>(()) /// ``` /// /// Errors if the stream could not write. pub fn write_command(&mut self, command: Command) -> Result<(), Error> { use Command::*; let computed = match command { CAP(mode) => { use CapMode::*; Cow::Borrowed(match mode { LS => "CAP LS 302", END => "CAP END", }) as Cow } JOIN(channel) => { let formatted = format!("JOIN {}", 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 } OTHER(_) => { return Err(Error::new( std::io::ErrorKind::Other, "Cannot write commands of type OTHER", )); } PING(code) => { let formatted = format!("PING {}", code); 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 } USER(username, s1, s2, realname) => { let formatted = format!("USER {} {} {} :{}", username, s1, s2, realname); Cow::Owned(formatted) as Cow } }; self.write(&computed)?; Ok(()) } // Utility functions! /// Helper function for sending PRIVMSGs. /// This makes /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.send_privmsg("#main", "Hello")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// /// equivalent to /// /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.write_command(Command::PRIVMSG("#main".to_string(), "Hello".to_string()))?; /// # Ok::<(), std::io::Error>(()) /// ``` pub fn send_privmsg(&mut self, channel: &str, message: &str) -> Result<(), Error> { self.write_command(Command::PRIVMSG(channel.to_string(), message.to_string()))?; Ok(()) } /// Helper function for sending JOINs. /// This makes /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.send_join("#main")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// /// equivalent to /// /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.write_command(Command::JOIN("#main".to_string()))?; /// # Ok::<(), std::io::Error>(()) /// ``` pub fn send_join(&mut self, channel: &str) -> Result<(), Error> { self.write_command(Command::JOIN(channel.to_string()))?; Ok(()) } /// Helper function for sending MODEs. /// This makes /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.send_mode("test", Some("+B"))?; /// # Ok::<(), std::io::Error>(()) /// ``` /// /// equivalent to /// /// ```no_run /// # use circe::*; /// # let mut client = Client::new(Config::from_toml("config.toml")?)?; /// client.write_command(Command::MODE("test".to_string(), Some("+B".to_string())))?; /// # Ok::<(), std::io::Error>(()) /// ``` pub fn send_mode(&mut self, target: &str, mode: Option<&str>) -> Result<(), Error> { if let Some(mode) = mode { self.write_command(Command::MODE(target.to_string(), Some(mode.to_string())))?; } else { self.write_command(Command::MODE(target.to_string(), None))?; } Ok(()) } } impl Config { /// Create a new config for the client
///
/// channels: Channels to join on the IRC
/// host: IP or domain of the IRC server
/// mode: Mode to join the IRC with (optional)
/// nickname: Nickname to join the IRC with (optional, defaults to the given username)
/// port: Port of the IRC server
/// username: Username to join the IRC with
/// ```rust /// # use circe::*; /// let config = Config::new( /// Box::new(["#main".to_string(), "#main2".to_string()]), /// "192.168.178.100", /// Some("+B".to_string()), /// Some("IRSC".to_string()), /// 6667, /// "IRSC", /// ); /// ``` pub fn new( channels: Box<[String]>, host: &str, mode: Option, nickname: Option, port: u16, username: &str, ) -> Self { Self { channels, host: host.into(), mode, nickname, port, username: username.into(), } } /// Create a config from a toml file /// ```no_run /// # use circe::*; /// let config = Config::from_toml("config.toml")?; /// # Ok::<(), std::io::Error>(()) /// ``` /// /// ```toml /// channels = ["#main", "#main2"] /// host = "192.168.178.100" /// mode = "+B" /// nickname = "IRSC" /// port = 6667 /// username = "IRSC" /// ``` /// /// Returns an Error if the file cannot be opened or if the TOML is invalid 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)) }) } }