circe/src/lib.rs

451 lines
13 KiB
Rust

//! 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<String>,
nickname: Option<String>,
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<String>,
),
#[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::<Vec<&str>>()[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<Self, Error> {
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<String> {
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<Command, ()> {
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<str>
};
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<str>
}
JOIN(channel) => {
let formatted = format!("JOIN {}", 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>
}
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<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>
}
USER(username, s1, s2, realname) => {
let formatted = format!("USER {} {} {} :{}", username, s1, s2, realname);
Cow::Owned(formatted) as Cow<str>
}
};
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<br>
/// <br>
/// channels: Channels to join on the IRC<br>
/// host: IP or domain of the IRC server<br>
/// mode: Mode to join the IRC with (optional)<br>
/// nickname: Nickname to join the IRC with (optional, defaults to the given username)<br>
/// port: Port of the IRC server<br>
/// username: Username to join the IRC with<br>
/// ```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<String>,
nickname: Option<String>,
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<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))
})
}
}