circe 0.1.5

IRC crate in Rust
Documentation
//! 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_attr(docsrs, feature(doc_cfg))]

#[cfg(feature = "tls")]
use native_tls::TlsConnector;

use std::borrow::Cow;
use std::io::{Error, Read, Write};
use std::net::TcpStream;

#[cfg(feature = "toml_support")]
use serde_derive::Deserialize;
#[cfg(feature = "toml_support")]
use std::fs::File;
#[cfg(feature = "toml_support")]
use std::path::Path;

/// 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<TcpStream>,
}

/// Config for the IRC client
#[derive(Clone, Default)]
#[cfg_attr(feature = "toml_support", derive(Deserialize))]
pub struct Config {
    channels: Vec<String>,
    host: String,
    mode: Option<String>,
    nickname: Option<String>,
    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<Self, Error> {
        let stream = TcpStream::connect(format!("{}:{}", config.host, config.port)).unwrap();
        #[cfg(feature = "tls")]
        let sslstream: native_tls::TlsStream<TcpStream>;

        #[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<String> {
        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<commands::Command, NoNewLines> {
        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<str>
        };

        #[cfg(feature = "debug")]
        println!("{:?}", formatted);

        self.stream.write_all(formatted.as_bytes())?;

        Ok(())
    }

    /// Send a [`commands::Command`] to the IRC.<br>
    /// 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<str>
            }
            AWAY(message) => {
                let formatted = format!("AWAY {}", message);
                Cow::Owned(formatted) as Cow<str>
            }
            CAP(mode) => {
                use commands::CapMode::{END, LS};
                Cow::Borrowed(match mode {
                    LS => "CAP LS 302",
                    END => "CAP END",
                }) as Cow<str>
            }
            INVITE(username, channel) => {
                let formatted = format!("INVITE {} {}", username, channel);
                Cow::Owned(formatted) as Cow<str>
            }
            JOIN(channel) => {
                let formatted = format!("JOIN {}", channel);
                Cow::Owned(formatted) as Cow<str>
            }
            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<str>
            }
            NAMES(channel, server) => {
                let formatted = {
                    if let Some(server) = server {
                        format!("NAMES {} {}", channel, server)
                    } else {
                        format!("NAMES {}", channel)
                    }
                };
                Cow::Owned(formatted) as Cow<str>
            }
            NICK(nickname) => {
                let formatted = format!("NICK {}", nickname);
                Cow::Owned(formatted) as Cow<str>
            }
            MODE(target, mode) => {
                let formatted = {
                    if let Some(mode) = mode {
                        format!("MODE {} {}", target, mode)
                    } else {
                        format!("MODE {}", target)
                    }
                };
                Cow::Owned(formatted) as Cow<str>
            }
            OPER(nick, password) => {
                let formatted = format!("OPER {} {}", nick, password);
                Cow::Owned(formatted) as Cow<str>
            }
            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<str>
            }
            PASS(password) => {
                let formatted = format!("PASS {}", password);
                Cow::Owned(formatted) as Cow<str>
            }
            PING(target) => {
                let formatted = format!("PING {}", target);
                Cow::Owned(formatted) as Cow<str>
            }
            PONG(code) => {
                let formatted = format!("PONG {}", code);
                Cow::Owned(formatted) as Cow<str>
            }
            PRIVMSG(_, target, message) => {
                let formatted = format!("PRIVMSG {} {}", target, message);
                Cow::Owned(formatted) as Cow<str>
            }
            QUIT(message) => {
                let formatted = format!("QUIT :{}", message);
                Cow::Owned(formatted) as Cow<str>
            }
            TOPIC(channel, topic) => {
                let formatted = {
                    if let Some(topic) = topic {
                        format!("TOPIC {} :{}", channel, topic)
                    } else {
                        format!("TOPIC {}", channel)
                    }
                };
                Cow::Owned(formatted) as Cow<str>
            }
            USER(username, s1, s2, realname) => {
                let formatted = format!("USER {} {} {} :{}", username, s1, s2, realname);
                Cow::Owned(formatted) as Cow<str>
            }
        };

        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<String>;
        if let Some(mode) = mode {
            mode_config = Some(mode.to_string());
        } else {
            mode_config = None;
        }

        let nickname_config: Option<String>;
        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(),
        }
    }

    /// 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_attr(docsrs, doc(cfg(feature = "toml_support")))]
    #[cfg(feature = "toml_support")]
    pub fn from_toml<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
        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))
        })
    }
}