diff options
| author | Max Audron <audron@cocaine.farm> | 2021-10-22 19:08:59 +0200 |
|---|---|---|
| committer | Max Audron <audron@cocaine.farm> | 2021-10-22 19:09:39 +0200 |
| commit | 309899168a086de88acf97fd6683387a7af7078c (patch) | |
| tree | 846075c1e9af0d7139edae5597f1147b851ed2b2 | |
| parent | remove wolfram alpha url shortening (diff) | |
write tons of documentation and reorganize some modules
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | src/config.rs | 120 | ||||
| -rw-r--r-- | src/hooks/intensify.rs | 1 | ||||
| -rw-r--r-- | src/hooks/mod.rs | 20 | ||||
| -rw-r--r-- | src/hooks/pet.rs | 9 | ||||
| -rw-r--r-- | src/hooks/sed/mod.rs | 19 | ||||
| -rw-r--r-- | src/hooks/sed/parser.rs | 2 | ||||
| -rw-r--r-- | src/hooks/shifty_eyes.rs | 2 | ||||
| -rw-r--r-- | src/hooks/wolfram_alpha.rs | 8 | ||||
| -rw-r--r-- | src/lib.rs | 55 | ||||
| -rw-r--r-- | src/main.rs | 2 | ||||
| -rw-r--r-- | src/util/formatting/color.rs | 1 | ||||
| -rw-r--r-- | src/util/formatting/mod.rs | 57 | ||||
| -rw-r--r-- | src/util/formatting/truncate.rs | 1 | ||||
| -rw-r--r-- | src/util/mod.rs | 9 | ||||
| -rw-r--r-- | src/util/web/mod.rs (renamed from src/util/web.rs) | 34 | ||||
| -rw-r--r-- | src/util/web/url_shorteners.rs | 21 |
18 files changed, 300 insertions, 65 deletions
@@ -133,7 +133,7 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "catinator" -version = "1.6.3" +version = "1.6.4" dependencies = [ "anyhow", "async-trait", @@ -1,6 +1,6 @@ [package] name = "catinator" -version = "1.6.3" +version = "1.6.4" authors = ["Max Audron <audron@cocaine.farm>"] edition = "2018" diff --git a/src/config.rs b/src/config.rs index 4429184..ec79591 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,101 @@ +//! The bots main configuration is housed in the [Config] struct. +//! +//! The configuration is by default loaded from `config.toml` in the current directory, +//! or from the path that is set by the `CATINATOR_CONFIG` variable. Environment +//! variables prefixed with `CATINATOR_` will also be loaded as configuration. +//! +//! The configuration uses [figment](https://docs.rs/figment) for it's configuration. +//! +//! # Profiles +//! The configuration is setup to use different profiles depending on how it is compiled. +//! +//! The main configuration file `config.toml` specifies the `default` profile which +//! sets default variables for most things. The `release` profile is also specified +//! in `config.toml` and selected when being built with the `--release` flag. +//! Additional configuration to set a different user while testing can be set in +//! the `debug` profile, which is selected if built without the `--release` flag. +//! +//! When compiled in release mode only the the `config.toml` or `CATINATOR_CONFIG` +//! and environment variables will be loaded. +//! +//! When compiled in debug mode an additional `config.debug.toml` is loaded +//! from the current directory. You can use this to set different channels and +//! nickname for testing. +//! +//! While developing this library / bot the `config.debug.toml` is also ignored from git. +//! +//! # Example +//! ```toml +//! [default] +//! [default.user] +//! # The username used for sasl and nickserv authentication +//! username = "catinator" +//! realname = "moaw" +//! +//! [default.server] +//! hostname = "<host>" +//! port = 6697 +//! tls = true +//! +//! # Enabled sasl, also requires user.password to be set +//! sasl = true +//! +//! [default.settings] +//! # The prefix to use for commands +//! # Example: ":about" +//! prefix = ':' +//! +//! [release] +//! [release.user] +//! # The backslash has to be escaped here +//! nickname = "\\__{^-_-^}" +//! [release.server] +//! channels = ["<channel 1>", "<channel 2>"] +//! ``` +//! +//! # Configuration for hooks +//! +//! If you write hooks that require some configuration you can use the +//! [`Bot::figment`](crate::Bot::figment) function to retrieve the figment and extract your own +//! configuration. +//! +//! ## Example: +//! ``` +//! use serde::{Serialize, Deserialize}; +//! use anyhow::{Context, Result}; +//! use irc::client::prelude::*; +//! use figment::providers::Env; +//! +//! use catinator::Bot; +//! +//! // Define a struct to initialize that contains your configuration +//! // you need to derive serde's Serialize and Deserialize on it +//! #[derive(Clone, Debug, Deserialize, Serialize)] +//! pub struct WolframAlpha { +//! wa_api_key: String, +//! } +//! +//! impl WolframAlpha { +//! // Impl a `new()` function that gets passed a reference to the +//! // bot, or just a reference to the figment. +//! pub fn new(bot: &Bot) -> Result<WolframAlpha> { +//! // Get the figment, we have to clone it here to merge our own env var config in. +//! bot.figment +//! .clone() +//! .merge(Env::prefixed("CATINATOR_")) +//! // Extract the config into the return type of this function +//! .extract() +//! .context("failed to extract wolfram alpha config") +//! } +//! +//! pub async fn wa(&self, bot: &Bot, msg: Message) -> Result<()> { +//! // Impl your command, hook or matcher here to allow it to access it's configuration +//! +//! Ok(()) +//! } +//! } +//! ``` + use serde::{Deserialize, Serialize}; use figment::{ @@ -7,10 +105,14 @@ use figment::{ }; use tracing::debug; +/// Configuration for the Bot #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] pub struct Config { + /// Settings related to the [User] pub user: User, + /// Settings related to the [Server] pub server: Server, + /// General bot related [Settings] pub settings: Settings, } @@ -30,24 +132,36 @@ impl From<Config> for irc::client::prelude::Config { } } +/// User related configuration #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] pub struct User { + /// The displayed nickname of the bot pub nickname: String, + /// The username used for authentication pub username: String, + /// The password used for authentication + /// Defaults to None #[serde(default)] pub password: Option<String>, + /// The bots realname pub realname: String, } +/// Connection info for the irc server #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] pub struct Server { + /// Hostname to connect to pub hostname: String, + /// Port to connect to (default: 6697 TLS) #[serde(default = "default_port")] pub port: u16, + /// Wether or not to use TLS (default: true) #[serde(default = "default_tls")] pub tls: bool, + /// Enable or disable sasl authentication (default: false) #[serde(default)] pub sasl: bool, + /// Channels to join (default: []) #[serde(default)] pub channels: Vec<String>, } @@ -60,8 +174,10 @@ const fn default_tls() -> bool { true } +/// General settings for the bot #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] pub struct Settings { + /// The prefix used for commands like `:about` (default: ':') #[serde(default = "default_prefix")] pub prefix: char, // pub wa_api_key: String, @@ -72,12 +188,12 @@ const fn default_prefix() -> char { } impl Config { - // Allow the configuration to be extracted from any `Provider`. + /// Allow the configuration to be extracted from any [`figment::Provider`]. pub fn from<T: Provider>(provider: T) -> Result<Config, Error> { Figment::from(provider).extract() } - // Provide a default provider, a `Figment`. + /// Provide a default provider, a `Figment`. pub fn figment() -> Figment { use figment::providers::Env; diff --git a/src/hooks/intensify.rs b/src/hooks/intensify.rs index 8e8a817..bbed9d9 100644 --- a/src/hooks/intensify.rs +++ b/src/hooks/intensify.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use irc::client::prelude::*; use macros::privmsg; +/// \[Turn things up to eleven\]. Intensifies things written in brackets. pub fn intensify(bot: &crate::Bot, msg: Message) -> Result<()> { privmsg!(msg, { let mut chars = text.chars(); diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 86d5697..822f839 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -1,17 +1,22 @@ -extern crate rand; +//! The bots hooks, commands and matchers. For explanation of different types see [crate::catinator] +//! +//! # Implementing hooks use anyhow::Result; use irc::client::prelude::*; -pub mod intensify; -pub mod pet; +mod intensify; +mod pet; +mod shifty_eyes; + +pub use intensify::*; +pub use pet::*; +pub use shifty_eyes::*; + pub mod sed; -pub mod shifty_eyes; pub mod wolfram_alpha; -pub use intensify::intensify; -pub use shifty_eyes::shifty_eyes; - +/// Replies with some information about the bot pub fn about(bot: &crate::Bot, msg: Message) -> Result<()> { bot.send_privmsg( msg.response_target().unwrap(), @@ -26,6 +31,7 @@ pub fn about(bot: &crate::Bot, msg: Message) -> Result<()> { Ok(()) } +/// Listen to AUTHENTICATE messages and perform SASL authentication pub fn sasl(bot: &crate::Bot, msg: Message) -> Result<()> { match msg.command { Command::AUTHENTICATE(text) => { diff --git a/src/hooks/pet.rs b/src/hooks/pet.rs index 3e2defe..d11f562 100644 --- a/src/hooks/pet.rs +++ b/src/hooks/pet.rs @@ -1,5 +1,3 @@ -use std::str; - use anyhow::{Context, Result}; use irc::client::prelude::*; use macros::privmsg; @@ -15,14 +13,9 @@ const PET_RESPONSE: [&str; 5] = [ "strikes you with it's sharp claws", ]; -/// Pet cat +/// Pet the cat, get rekt /// /// Sends some random action when petted. -/// -/// # See also -/// -/// - [`Bot::send_action`] -/// - RESPONSE pub fn pet(bot: &crate::Bot, msg: Message) -> Result<()> { privmsg!(msg, { bot.send_action( diff --git a/src/hooks/sed/mod.rs b/src/hooks/sed/mod.rs index 3128372..7b7906c 100644 --- a/src/hooks/sed/mod.rs +++ b/src/hooks/sed/mod.rs @@ -1,3 +1,22 @@ +//! sed for irc. replace text in past messages +//! +//! Perform sed replaces on single messages in the buffers history, up to 10k messages old. +//! +//! ```text +//! <+basso> crystalmett: welcome back +//! <%audron> s/wel/whale/ +//! <\__{^-_-^}> <basso> crystalmett: whalecome back +//! ``` +//! +//! # Supported flags +//! ```text +//! g global: match every occurrence of the regex in a line +//! i case-insensitive: letters match both upper and lower case +//! s allow . to match \n +//! U swap the meaning of x* and x*? +//! x ignore whitespace and allow line comments (starting with `#`) +//! ``` + use anyhow::{anyhow, bail, Context, Result}; use irc::client::prelude::*; diff --git a/src/hooks/sed/parser.rs b/src/hooks/sed/parser.rs index eb7ef3e..815803c 100644 --- a/src/hooks/sed/parser.rs +++ b/src/hooks/sed/parser.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, str::Chars}; use bitflags::bitflags; use regex::Regex; -use crate::util::formatting::Formatting; +use crate::util::Formatting; type Commands = Vec<Command>; diff --git a/src/hooks/shifty_eyes.rs b/src/hooks/shifty_eyes.rs index 4680007..6203075 100644 --- a/src/hooks/shifty_eyes.rs +++ b/src/hooks/shifty_eyes.rs @@ -4,6 +4,7 @@ use irc::client::prelude::*; const EYES: [char; 11] = ['^', 'v', 'V', '>', '<', 'x', 'X', '-', 'o', 'O', '.']; const NOSE: [char; 7] = ['.', '_', '-', ';', '\'', '"', '~']; +/// you are being watched <.< pub fn shifty_eyes(bot: &crate::Bot, msg: Message) -> Result<()> { if let Command::PRIVMSG(_, text) = msg.command.clone() { if text.len() == 3 { @@ -41,6 +42,7 @@ fn invert(input: char) -> Result<char> { '>' => Ok('<'), '<' => Ok('>'), 'x' => Ok('o'), + '.' => Ok('o'), 'X' => Ok('O'), '-' => Ok('o'), 'o' => Ok('-'), diff --git a/src/hooks/wolfram_alpha.rs b/src/hooks/wolfram_alpha.rs index cdd32ca..9f56df4 100644 --- a/src/hooks/wolfram_alpha.rs +++ b/src/hooks/wolfram_alpha.rs @@ -1,7 +1,7 @@ -use crate::util::{ - formatting::truncate, - web::{quote_plus, IsgdUrlShortener, UrlShortener}, -}; +//! ask wolfram alpha a query + +use crate::util::{quote_plus, truncate}; +// use crate::util::{url_shorteners::Isgd, UrlShortener}; use anyhow::{bail, Context, Error, Result}; use figment::providers::Env; use futures::join; @@ -1,3 +1,35 @@ +//! catinator is a general purpose irc bot making crate. +//! +//! Most of the heavy lifting is done by the [irc](https://docs.rs/irc) crate, +//! but catinator adds useful high level utilities to make the bot making easier. +//! +//! Example: +//! ```no_run +//! #[tokio::main] +//! async fn main() { +//! tracing_subscriber::fmt::init(); +//! +//! // Initialize the bot, loading the config and establishing the connection +//! let mut bot = catinator::Bot::new().await.unwrap(); +//! +//! // Setup any modules that require it. +//! let wolfram_alpha = catinator::hooks::wolfram_alpha::WolframAlpha::new(&bot) +//! .expect("failed to initialize WolframAlpha command"); +//! +//! // Call the catinator macro to setup the hooks, matchers and commands +//! catinator::catinator![ +//! // For example add the sasl hook to handle sasl authentication +//! hook("sasl", "Handle Authentication.", AUTHENTICATE, catinator::hooks::sasl), +//! +//! // Add a matcher that executes on a specific regex +//! matcher("shifty_eyes", ">.>", r"^\S{3}$", catinator::hooks::shifty_eyes), +//! +//! // Add an async command that calls a method on the previously instantiated struct. +//! async command("wa", "Returns Wolfram Alpha results for a query", wolfram_alpha.wa), +//! ]; +//! } +//! ``` + #![cfg_attr(all(test, feature = "bench"), feature(test))] #[cfg(all(test, feature = "bench"))] extern crate test; @@ -10,22 +42,24 @@ pub mod config; pub mod hooks; pub mod util; +// Rexport of the catinator proc macros pub use macros::catinator; -#[macro_export] -macro_rules! reply { - ( $msg:expr, $text:expr ) => { - bot.send_privmsg($msg.response_target().unwrap(), $text.as_str())?; - }; -} - +/// The struct handling bot actions and configuration pub struct Bot { + /// The base config of the bot pub config: config::Config, + /// The figment the config is extracted from pub figment: figment::Figment, + /// The irc client object, used to send messages etc + /// It is recommended to use the methods directly on the Bot struct instead. pub irc_client: irc::client::Client, } impl Bot { + /// Initializes the bot. + /// Loads configuration from `CATINATOR_` environment variables and the `config.toml` file + /// Starts the connection to the irc server. pub async fn new() -> Result<Bot> { let figment = config::Config::figment(); let config: config::Config = figment.extract().context("failed to extract config")?; @@ -46,10 +80,14 @@ impl Bot { Ok(bot) } + /// Get the bots figment to use when building your own configuration. + /// See [config] pub fn figment(&self) -> &figment::Figment { &self.figment } + /// Initialize a sasl connection, you usually don't need + /// to run this yourself as it is done during [Bot::new]. pub async fn sasl_init(&self) -> Result<()> { self.irc_client .send_cap_req(&vec![irc::client::prelude::Capability::Sasl])?; @@ -65,6 +103,7 @@ impl Bot { Ok(()) } + /// Send a privmsg to the target `#channel` or `user` pub fn send_privmsg( &self, target: &str, @@ -73,6 +112,7 @@ impl Bot { self.irc_client.send_privmsg(target, message) } + /// Send a notice to the target `#channel` or `user` pub fn send_notice( &self, target: &str, @@ -81,6 +121,7 @@ impl Bot { self.irc_client.send_notice(target, message) } + /// Send an action (`/me`) to the target `#channel` or `user` pub fn send_action( &self, target: &str, diff --git a/src/main.rs b/src/main.rs index 747948e..66148fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ async fn main() { command( "pet", "Pet the cat, cats generally like pets.", - catinator::hooks::pet::pet + catinator::hooks::pet ), command( "about", diff --git a/src/util/formatting/color.rs b/src/util/formatting/color.rs index 6907805..ac78b9c 100644 --- a/src/util/formatting/color.rs +++ b/src/util/formatting/color.rs @@ -1,3 +1,4 @@ +/// The basic 16 IRC color codes pub enum Color { White = 00, Black = 01, diff --git a/src/util/formatting/mod.rs b/src/util/formatting/mod.rs index 5cfdfa8..a20b48f 100644 --- a/src/util/formatting/mod.rs +++ b/src/util/formatting/mod.rs @@ -1,9 +1,31 @@ +//! Tools for formatting irc messages + mod truncate; mod color; pub use truncate::*; pub use color::*; +/// Turn strings bold, italic,underline, strikethrough, and monospace. +/// +/// The Formatting trait is implemented on `String` and `&str` for your convenience +/// +/// # Format a string +/// ``` +/// use catinator::util::Formatting; +/// +/// let text = "this should be bold"; +/// assert_eq!("\x02this should be bold\x02", text.bold()) +/// ``` +/// +/// # Use raw formatting code +/// ``` +/// use catinator::util::Formatting; +/// +/// let text = "this should be bold"; +/// let result = format!("{}{}", String::BOLD, text); +/// assert_eq!("\x02this should be bold", result) +/// ``` pub trait Formatting<'r> { const BOLD: &'r str = "\x02"; const ITALIC: &'r str = "\x1D"; @@ -12,11 +34,11 @@ pub trait Formatting<'r> { const MONOSPACE: &'r str = "\x11"; const COLOR: &'r str = "\x03"; - fn bold(self) -> Self; - fn italic(self) -> Self; - fn underline(self) -> Self; - fn strikethrough(self) -> Self; - fn monospace(self) -> Self; + fn bold(self) -> String; + fn italic(self) -> String; + fn underline(self) -> String; + fn strikethrough(self) -> String; + fn monospace(self) -> String; // fn color(self, color: Color) -> Self; } @@ -65,6 +87,31 @@ impl<'r> Formatting<'r> for String { // } } +impl<'r> Formatting<'r> for &str { + fn bold(self) -> String { + format!("{}{}{}", Self::BOLD, self, Self::BOLD) + } + + fn italic(self) -> String { + format!("{}{}{}", Self::ITALIC, self, Self::ITALIC) + } + + fn underline(self) -> String { + format!("{}{}{}", Self::UNDERLINE, self, Self::UNDERLINE) + } + + fn strikethrough(self) -> String { + format!("{}{}{}", Self::STRIKETHROUGH, self, Self::STRIKETHROUGH) + } + + fn monospace(self) -> String { + format!("{}{}{}", Self::MONOSPACE, self, Self::MONOSPACE) + } + + // TODO implement color codes + // fn color(mut self, foreground: Option<Color>, background: Option<Color>) -> Self { } +} + #[cfg(all(test, feature = "bench"))] mod bench { use super::*; diff --git a/src/util/formatting/truncate.rs b/src/util/formatting/truncate.rs index c1b6257..80caa23 100644 --- a/src/util/formatting/truncate.rs +++ b/src/util/formatting/truncate.rs @@ -1,4 +1,5 @@ /// Truncates a string after a certain number of characters. +/// /// Function always tries to truncate on a word boundary. /// Reimplemented from gonzobot. pub fn truncate(text: &str, len: usize) -> String { diff --git a/src/util/mod.rs b/src/util/mod.rs index a8988c3..23ef2ce 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,2 +1,7 @@ -pub mod formatting; -pub mod web; +//! Utilities for dealing with IRC and bot making + +mod formatting; +mod web; + +pub use formatting::*; +pub use web::*; diff --git a/src/util/web.rs b/src/util/web/mod.rs index b9f44c1..4e886af 100644 --- a/src/util/web.rs +++ b/src/util/web/mod.rs @@ -1,36 +1,18 @@ -use anyhow::{Context, Error, Result}; +use anyhow::{Error, Result}; use async_trait::async_trait; -use reqwest::{get, Url}; use urlparse::quote_plus as urlparse_quote_plus; -#[async_trait] -pub trait UrlShortener { - fn new() -> Self; - async fn shorten(&self, url: &str) -> Result<String, Error>; -} - -pub struct IsgdUrlShortener {} +pub mod url_shorteners; +/// Shorten urls #[async_trait] -impl UrlShortener for IsgdUrlShortener { - fn new() -> Self { - Self {} - } - - async fn shorten(&self, url: &str) -> Result<String, Error> { - Ok(get(Url::parse(&format!( - "https://is.gd/create.php?format=simple&url={}", - url - )) - .context("Failed to parse url")?) - .await - .context("Failed to make request")? - .text() - .await - .context("failed to get request response text")?) - } +pub trait UrlShortener { + /// Call this method with the url you want shortened. + /// Returns the shortened url. + async fn shorten(url: &str) -> Result<String, Error>; } +/// quote strings to be URL save pub fn quote_plus(text: &str) -> Result<String, Error> { Ok(urlparse_quote_plus(text, b"")?) } diff --git a/src/util/web/url_shorteners.rs b/src/util/web/url_shorteners.rs new file mode 100644 index 0000000..74d62ce --- /dev/null +++ b/src/util/web/url_shorteners.rs @@ -0,0 +1,21 @@ +use anyhow::{Context, Error, Result}; +use async_trait::async_trait; +use reqwest::{get, Url}; + +pub struct Isgd; + +#[async_trait] +impl super::UrlShortener for Isgd { + async fn shorten(url: &str) -> Result<String, Error> { + Ok(get(Url::parse(&format!( + "https://is.gd/create.php?format=simple&url={}", + url + )) + .context("Failed to parse url")?) + .await + .context("Failed to make request")? + .text() + .await + .context("failed to get request response text")?) + } +} |
