async-circe 0.2.3

IRC crate in Rust
Documentation
//! A simple IRC crate written in rust
//! ```run_fut
//! use async_circe::{commands::Command, Client, Config};
//!
//! #[tokio::main(flavor = "current_thread")]
//! async fn main() -> Result<(), tokio::io::Error> {
//!     let config = Config::default();
//!     let mut client = Client::new(config).await.unwrap();
//!     client.identify().await.unwrap();
//!
//!     loop {
//!         if let Some(command) = client.read().await? {
//!             if let Command::PRIVMSG(nick, channel, message) = command {
//!                 println!("{} in {}: {}", nick, channel, message);
//!             }
//!         }
//!     }
//! }
//! ```
//!
//! The crate requires `tokio` with the `macros` and `rt` feature enabled to function.<br>
//! For more examples (connecting to IRCs that dont use SSL, getting the config from
//! a toml file, debugging or just in general an overview how a project with async-circe
//! is structured) see the [examples](https://git.karx.xyz/circe/async-circe/src/branch/master/examples) folder on our git.

#![warn(missing_docs)] // We want everything documented
#![allow(clippy::needless_return)] // Wants to remove a return statement, but when it's removed the code doesn't compile
#![feature(doc_cfg)]

use tokio::io::BufReader;
use tokio::io::Error;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::net::TcpStream;

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

#[cfg(feature = "toml_config")]
use serde_derive::Deserialize;
#[cfg(feature = "toml_config")]
use std::fs::File;
#[cfg(feature = "toml_config")]
use std::io::Read;
#[cfg(any(doc, feature = "toml_config"))]
use std::path::Path;
#[cfg(feature = "toml_config")]
use tokio::io::ErrorKind;

pub mod commands;

/// An IRC client
pub struct Client {
    config: Config,
    #[cfg(not(feature = "tls"))]
    buf_reader: BufReader<TcpStream>,
    #[cfg(feature = "tls")]
    buf_reader: BufReader<tokio_native_tls::TlsStream<tokio::net::TcpStream>>,
}

/// Config for the IRC client<br>
/// For more information about what arguments the IRC commands take, see [`commands::Command`].
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "toml_config", derive(Deserialize))]
pub struct Config {
    channels: Vec<String>,
    host: String,
    mode: Option<String>,
    nickname: Option<String>,
    port: u16,
    username: String,
}

impl Client {
    /// Creates a new client with a given [`Config`].
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// let mut client = Client::new(config).await?;
    /// # Ok(())
    /// ```
    /// # 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 async fn new(config: Config) -> Result<Self, Error> {
        tracing::debug!("Creating TcpStream");
        let stream = TcpStream::connect(format!("{}:{}", config.host, config.port))
            .await
            .unwrap();
        tracing::debug!("New TcpStream created");

        #[cfg(feature = "tls")]
        {
            let connector = TlsConnector::builder().build().unwrap();
            let async_connector = tokio_native_tls::TlsConnector::from(connector);

            let sslstream = async_connector
                .connect(config.host.as_str(), stream)
                .await
                .unwrap();

            let buf_reader = BufReader::new(sslstream);

            return Ok(Self { config, buf_reader });
        };

        #[cfg(not(feature = "tls"))]
        {
            let buf_reader = BufReader::new(stream);
            return Ok(Self { config, buf_reader });
        };
    }

    /// Identify user and joins the in the [`Config`] specified channels.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// client.identify().await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns error if the client could not write to the stream.
    pub async fn identify(&mut self) -> Result<(), Error> {
        self.cap_mode(commands::CapMode::LS).await?;
        self.cap_mode(commands::CapMode::END).await?;

        self.user(
            self.config.username.clone(),
            "*",
            "*",
            self.config.username.clone(),
        )
        .await?;

        if let Some(nick) = self.config.nickname.clone() {
            self.nick(&nick).await?;
        } else {
            self.nick(&self.config.username.clone()).await?;
        }

        loop {
            if let Some(command) = self.read().await? {
                match command {
                    commands::Command::PING(code) => {
                        self.pong(&code).await?;
                    }
                    commands::Command::OTHER(line) => {
                        tracing::trace!("{}", line);
                        if line.contains("001") {
                            break;
                        }
                    }
                    _ => {}
                }
            }
        }

        let config = self.config.clone();
        let user = {
            if let Some(nick) = config.nickname {
                nick
            } else {
                config.username
            }
        };

        if let Some(mode) = config.mode {
            self.mode(&user, Some(&mode)).await?;
        }

        for channel in &config.channels {
            self.join(channel).await?;
        }

        tracing::info!("async-circe has identified");
        Ok(())
    }

    async fn read_string(&mut self) -> Result<Option<String>, tokio::io::Error> {
        let mut buffer = String::new();

        let num_bytes = match self.buf_reader.read_line(&mut buffer).await {
            Ok(b) => b,
            Err(e) => {
                tracing::error!("{}", e);
                return Err(e);
            }
        };

        if num_bytes == 0 {
            return Ok(None);
        }

        Ok(Some(buffer))
    }

    /// Read data coming from the IRC as a [`commands::Command`].
    /// # Example
    /// ```run_fut
    /// # use async_circe::*;
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// loop {
    ///    if let Some(ref command) = client.read().await? {
    ///        if let commands::Command::PRIVMSG(nick, channel, message) = command {
    ///            println!("{} in {}: {}", nick, channel, message);
    ///        }
    ///    }
    /// # break;
    /// }
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn read(&mut self) -> Result<Option<commands::Command>, tokio::io::Error> {
        if let Some(string) = self.read_string().await? {
            let command = commands::Command::command_from_str(&string).await;

            if let commands::Command::PING(command) = command {
                if let Err(_e) = self.pong(&command).await {
                    return Ok(None);
                }
                return Ok(Some(commands::Command::PONG("".to_string())));
            }

            return Ok(Some(command));
        }

        Ok(None)
    }

    async fn write(&mut self, data: String) -> Result<(), Error> {
        tracing::trace!("{:?}", data);
        self.buf_reader.write_all(data.as_bytes()).await?;

        Ok(())
    }

    /// Request information about the admin of a given server.
    /// # Example
    /// ```run_fut
    /// # use async_circe::*;
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.admin("libera.chat").await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn admin(&mut self, target: &str) -> Result<(), Error> {
        self.write(format!("ADMIN {}\r\n", target)).await?;
        Ok(())
    }

    /// Set the status of the client.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.away("afk").await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn away(&mut self, message: &str) -> Result<(), Error> {
        self.write(format!("AWAY {}\r\n", message)).await?;
        Ok(())
    }

    #[doc(hidden)]
    pub async fn cap_mode(&mut self, mode: commands::CapMode) -> Result<(), Error> {
        let cap_mode = match mode {
            commands::CapMode::LS => "CAP LS 302\r\n",
            commands::CapMode::END => "CAP END\r\n",
        };
        self.write(cap_mode.to_string()).await?;
        Ok(())
    }

    /// Invite someone to a channel.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.invite("liblemonirc", "#async-circe").await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn invite(&mut self, username: &str, channel: &str) -> Result<(), Error> {
        self.write(format!("INVITE {} {}\r\n", username, channel))
            .await?;
        Ok(())
    }

    /// Join a channel.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.join("#chaos").await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn join(&mut self, channel: &str) -> Result<(), Error> {
        self.write(format!("JOIN {}\r\n", channel)).await?;
        Ok(())
    }

    /// List available channels on an IRC, or users in a channel.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.list(None, None).await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn list(&mut self, channel: Option<&str>, server: Option<&str>) -> Result<(), Error> {
        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());
        }
        formatted.push_str("\r\n");
        self.write(formatted).await?;
        Ok(())
    }

    /// Set the mode for a user.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.mode("test", Some("+B")).await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn mode(&mut self, target: &str, mode: Option<&str>) -> Result<(), Error> {
        if let Some(mode) = mode {
            self.write(format!("MODE {} {}\r\n", target, mode,)).await?;
        } else {
            self.write(format!("MODE {}\r\n", target)).await?;
        }
        Ok(())
    }

    /// Get all the people online in channels.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.names("#chaos,#async-circe", None).await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn names(&mut self, channel: &str, server: Option<&str>) -> Result<(), Error> {
        if let Some(server) = server {
            self.write(format!("NAMES {} {}\r\n", channel, server))
                .await?;
        } else {
            self.write(format!("NAMES {}\r\n", channel)).await?;
        }
        Ok(())
    }

    /// Change your nickname on a server.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.nick("Not async-circe").await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn nick(&mut self, nickname: &str) -> Result<(), Error> {
        self.write(format!("NICK {}\r\n", nickname)).await?;
        Ok(())
    }

    /// Authentificate as an operator on a server.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.oper("username", "password").await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn oper(&mut self, username: &str, password: &str) -> Result<(), Error> {
        self.write(format!("OPER {} {}\r\n", username, password))
            .await?;
        Ok(())
    }

    /// Leave a channel.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.part("#chaos").await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn part(&mut self, channel: &str) -> Result<(), Error> {
        self.write(format!("PART {}\r\n", channel)).await?;
        Ok(())
    }

    #[doc(hidden)]
    pub async fn pass(&mut self, password: &str) -> Result<(), Error> {
        self.write(format!("PASS {}\r\n", password)).await?;
        Ok(())
    }

    /// Tests the presence of a connection to a server.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.ping("libera.chat", None).await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn ping(&mut self, server1: &str, server2: Option<&str>) -> Result<(), Error> {
        if let Some(server2) = server2 {
            self.write(format!("PING {} {}\r\n", server1, server2))
                .await?;
        } else {
            self.write(format!("PING {}\r\n", server1)).await?;
        }
        // TODO look if we actually get a PONG back
        Ok(())
    }

    #[doc(hidden)]
    pub async fn pong(&mut self, ping: &str) -> Result<(), Error> {
        self.write(format!("PONG {}\r\n", ping)).await?;
        Ok(())
    }

    /// Send a message to a channel.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.privmsg("#chaos", "Hello").await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn privmsg(&mut self, channel: &str, message: &str) -> Result<(), Error> {
        self.write(format!("PRIVMSG {} {}\r\n", channel, message))
            .await?;
        Ok(())
    }

    /// Leave the IRC server you are connected to.
    /// # Example
    /// ```run_fut
    /// # use async_circe::*;
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.quit(None).await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn quit(&mut self, message: Option<&str>) -> Result<(), Error> {
        if let Some(message) = message {
            self.write(format!("QUIT :{}\r\n", message)).await?;
        } else {
            self.write(format!(
                "QUIT :async-circe {} (https://crates.io/crates/async-circe)\r\n",
                env!("CARGO_PKG_VERSION")
            ))
            .await?;
        }
        tracing::info!("async-circe shutting down");
        self.buf_reader.shutdown().await?;
        Ok(())
    }

    /// Get the topic of a channel.
    /// # Example
    /// ```run_fut
    /// # use async_circe::*;
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.topic("#chaos", None).await?;
    /// # Ok(())
    /// ```
    /// Set the topic of a channel.
    /// # Example
    /// ```run_fut
    /// # let config = Default::default();
    /// # let mut client = Client::new(config).await?;
    /// # client.identify().await?;
    /// client.topic("#chaos", Some("CHAOS")).await?;
    /// # Ok(())
    /// ```
    /// # Errors
    /// Returns IO errors from the TcpStream.
    pub async fn topic(&mut self, channel: &str, topic: Option<&str>) -> Result<(), Error> {
        if let Some(topic) = topic {
            self.write(format!("TOPIC {} :{}\r\n", channel, topic))
                .await?;
        } else {
            self.write(format!("TOPIC {}\r\n", channel)).await?;
        }
        Ok(())
    }

    #[doc(hidden)]
    pub async fn user(
        &mut self,
        username: String,
        s1: &str,
        s2: &str,
        realname: String,
    ) -> Result<(), Error> {
        self.write(format!("USER {} {} {} :{}\r\n", username, s1, s2, realname))
            .await?;
        Ok(())
    }
}

impl Config {
    /// Create a new config for the client
    /// # Example
    /// ```rust
    /// # use async_circe::Config;
    /// let config = Config::new(
    ///     &["#chaos", "#async-circe"],
    ///     "karx.xyz",
    ///     Some("+B"),
    ///     Some("async-circe"),
    ///     6697,
    ///     "circe",
    /// );
    /// ```
    #[must_use]
    pub fn new(
        channels: &[&'static str],
        host: &str,
        mode: Option<&'static str>,
        nickname: Option<&'static str>,
        port: u16,
        username: &str,
    ) -> Self {
        tracing::info!("New async-circe client config");
        // 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());
        }

        let config = Self {
            channels: channels_config,
            host: host.into(),
            mode: mode_config,
            nickname: nickname_config,
            port,
            username: username.into(),
        };

        tracing::debug!("async-circe config: {:?}", config);
        config
    }

    /// Allows for configuring async-circe at runtime.
    /// # Example
    /// ```run_fut
    /// # use async_circe::Config;
    /// let config = Config::runtime_config(
    ///     vec!["#chaos".to_string(), "#async-circe".to_string()],
    ///     "karx.xyz".to_string(),
    ///     Some("+B".to_string()),
    ///     Some("async-circe".to_string()),
    ///     6697,
    ///     "circe".to_string(),
    /// );
    /// ```
    #[must_use]
    pub fn runtime_config(
        channels: Vec<String>,
        host: String,
        mode: Option<String>,
        nickname: Option<String>,
        port: u16,
        username: String,
    ) -> Self {
        tracing::info!("New async-circe client config");
        let mode_config: Option<String>;
        if let Some(mode) = mode {
            mode_config = Some(mode);
        } else {
            mode_config = None;
        }

        let nickname_config: Option<String>;
        if let Some(nickname) = nickname {
            nickname_config = Some(nickname);
        } else {
            nickname_config = Some(username.to_string());
        }

        let config = Self {
            channels,
            host,
            mode: mode_config,
            nickname: nickname_config,
            port,
            username,
        };

        tracing::debug!("async-circe config: {:?}", config);
        config
    }

    /// Create a config from a toml file
    /// # Example
    /// ```toml
    /// #Config.toml
    /// channels = ["#chaos", "#async-circe"]
    /// host = "karx.xyz"
    /// mode = "+B"
    /// nickname = "async-circe"
    /// port = 6667
    /// username = "circe"
    /// ```
    /// ```run_fut
    /// let config = Config::from_toml("Config.toml")?;
    /// ```
    /// # Errors
    /// Returns an Error if the file cannot be opened or if the TOML is invalid
    #[cfg(any(doc, feature = "toml_config"))]
    #[doc(cfg(feature = "toml_config"))]
    pub fn from_toml<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
        tracing::info!("New async-circe client config");
        let mut file = File::open(&path)?;
        let mut data = String::new();
        file.read_to_string(&mut data)?;

        let config = toml::from_str(&data)
            .map_err(|e| Error::new(ErrorKind::Other, format!("Invalid TOML: {}", e)));
        tracing::debug!("async-circe config: {:?}", config);
        config
    }
}