#![warn(missing_docs)] #![allow(clippy::needless_return)] #![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;
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>>,
}
#[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 {
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 });
};
}
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))
}
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(())
}
pub async fn admin(&mut self, target: &str) -> Result<(), Error> {
self.write(format!("ADMIN {}\r\n", target)).await?;
Ok(())
}
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(())
}
pub async fn invite(&mut self, username: &str, channel: &str) -> Result<(), Error> {
self.write(format!("INVITE {} {}\r\n", username, channel))
.await?;
Ok(())
}
pub async fn join(&mut self, channel: &str) -> Result<(), Error> {
self.write(format!("JOIN {}\r\n", channel)).await?;
Ok(())
}
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(())
}
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(())
}
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(())
}
pub async fn nick(&mut self, nickname: &str) -> Result<(), Error> {
self.write(format!("NICK {}\r\n", nickname)).await?;
Ok(())
}
pub async fn oper(&mut self, username: &str, password: &str) -> Result<(), Error> {
self.write(format!("OPER {} {}\r\n", username, password))
.await?;
Ok(())
}
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(())
}
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?;
}
Ok(())
}
#[doc(hidden)]
pub async fn pong(&mut self, ping: &str) -> Result<(), Error> {
self.write(format!("PONG {}\r\n", ping)).await?;
Ok(())
}
pub async fn privmsg(&mut self, channel: &str, message: &str) -> Result<(), Error> {
self.write(format!("PRIVMSG {} {}\r\n", channel, message))
.await?;
Ok(())
}
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(())
}
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 {
#[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");
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
}
#[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
}
#[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
}
}