diff options
| author | Max Audron <audron@cocaine.farm> | 2021-05-15 13:58:01 +0200 |
|---|---|---|
| committer | Max Audron <audron@cocaine.farm> | 2021-05-15 13:58:01 +0200 |
| commit | d0bff910b0b038ee85bc285bef7a63870a3474ab (patch) | |
| tree | b840d0b932c080fa5d5053b6bf66a91f94a80481 /src | |
init
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.rs | 46 | ||||
| -rw-r--r-- | src/hooks/mod.rs | 39 | ||||
| -rw-r--r-- | src/hooks/sed.rs | 175 | ||||
| -rw-r--r-- | src/hooks/shifty_eyes.rs | 46 | ||||
| -rw-r--r-- | src/lib.rs | 70 | ||||
| -rw-r--r-- | src/main.rs | 38 |
6 files changed, 414 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f7a0934 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize)] +pub struct Config { + pub user: User, + pub server: Server, + pub settings: Settings, +} + +impl From<Config> for irc::client::prelude::Config { + fn from(input: Config) -> Self { + Self { + nickname: Some(input.user.nickname), + username: Some(input.user.username), + realname: Some(input.user.realname), + nick_password: Some(input.user.password), + server: Some(input.server.hostname), + port: Some(input.server.port), + use_tls: Some(input.server.tls), + channels: input.server.channels, + ..irc::client::prelude::Config::default() + } + } +} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize)] +pub struct User { + pub nickname: String, + pub username: String, + pub password: String, + pub realname: String, +} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize)] +pub struct Server { + pub hostname: String, + pub port: u16, + pub tls: bool, + pub sasl: bool, + pub channels: Vec<String>, +} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Deserialize)] +pub struct Settings { + pub prefix: char, +} diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs new file mode 100644 index 0000000..ba4a98d --- /dev/null +++ b/src/hooks/mod.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use irc::client::prelude::*; + +pub mod sed; +pub mod shifty_eyes; + +pub use sed::*; +pub use shifty_eyes::shifty_eyes; + +pub fn sasl(bot: &crate::Bot, msg: Message) -> Result<()> { + match msg.command { + Command::AUTHENTICATE(text) => { + use sasl::client::mechanisms::Plain; + use sasl::client::Mechanism; + use sasl::common::Credentials; + + if text == "+" { + let creds = Credentials::default() + .with_username(bot.config.clone().user.username) + .with_password(bot.config.clone().user.password); + + let mut mechanism = Plain::from_credentials(creds).unwrap(); + + let initial_data = mechanism.initial(); + + bot.irc_client.send_sasl(base64::encode(initial_data))?; + bot.irc_client.send(Command::CAP( + None, + irc_proto::command::CapSubCommand::END, + None, + None, + ))?; + } + } + _ => (), + } + + Ok(()) +} diff --git a/src/hooks/sed.rs b/src/hooks/sed.rs new file mode 100644 index 0000000..095eb14 --- /dev/null +++ b/src/hooks/sed.rs @@ -0,0 +1,175 @@ +use anyhow::{anyhow, Result}; +use irc::client::prelude::*; + +use sedregex::ReplaceCommand; + +use std::cell::RefCell; + +static LOG_MAX_SIZE: usize = 10000; + +thread_local!(static LOG: RefCell<Vec::<(String, String)>> = RefCell::new(Vec::with_capacity(LOG_MAX_SIZE))); +thread_local!(static RE: regex::Regex = regex::Regex::new(r"^s/").unwrap()); + +pub fn log(_bot: &crate::Bot, msg: Message) -> Result<()> { + log_msg(msg) +} + +fn log_msg(msg: Message) -> Result<()> { + if let Command::PRIVMSG(_, text) = msg.command.clone() { + LOG.with(|log_cell| { + let mut log = log_cell.borrow_mut(); + if log.len() >= LOG_MAX_SIZE { + let _ = log.pop(); + } + log.push((msg.source_nickname().unwrap().to_string(), text)) + }); + } + Ok(()) +} + +pub fn replace(bot: &crate::Bot, msg: Message) -> Result<()> { + match find_and_replace(&msg) { + Ok(res) => { + bot.send_privmsg(msg.response_target().unwrap(), res.as_str()) + .unwrap(); + Ok(()) + } + Err(_) => Ok(()), + } +} + +fn find_and_replace(msg: &Message) -> Result<String> { + if let Command::PRIVMSG(_, text) = msg.command.clone() { + let cmd = match ReplaceCommand::new(text.as_str()) { + Ok(cmd) => cmd, + Err(_) => return Err(anyhow!("building replace command failed")), + }; + + return LOG.with(|log_cell| { + log_cell + .borrow() + .iter() + .rev() + .find(|(_, text)| cmd.expr.is_match(text) && !RE.with(|re| re.is_match(text))) + .and_then(|(nick, text)| Some(format!("<{}> {}", nick, cmd.execute(text)))) + .map_or(Err(anyhow!("replace failed")), |v| Ok(v)) + }); + } + + Err(anyhow!("not a privmsg")) +} + +#[cfg(test)] +mod tests { + use super::*; + use test::Bencher; + + fn populate_log() { + LOG.with(|log_cell| { + let mut log = log_cell.borrow_mut(); + log.push(( + "user".to_string(), + "this is a long message which will be replaced".to_string(), + )); + for _ in 0..LOG_MAX_SIZE-1 { + log.push(( + "user".to_string(), + "this is a long message which doesn't matter".to_string(), + )) + } + }); + } + + #[test] + fn test_log_limit() { + populate_log(); + + LOG.with(|log_cell| { + let log = log_cell.borrow(); + assert_eq!(log.len(), LOG_MAX_SIZE) + }); + + log_msg(Message { + tags: None, + prefix: Some(Prefix::Nickname( + "user".to_string(), + "username".to_string(), + "userhost".to_string(), + )), + command: Command::PRIVMSG( + "#channel".to_string(), + "this is the 10001th message".to_string(), + ), + }) + .unwrap(); + + LOG.with(|log_cell| { + let log = log_cell.borrow(); + assert_eq!(log.len(), LOG_MAX_SIZE) + }); + } + + #[test] + fn test_replace() { + populate_log(); + assert_eq!( + find_and_replace(&Message { + tags: None, + prefix: None, + command: Command::PRIVMSG( + "#channel".to_string(), + "s/will be/has been/".to_string(), + ), + }) + .unwrap(), + "<user> this is a long message which has been replaced" + ) + } + + #[test] + fn test_replace_complex() { + populate_log(); + assert_eq!( + find_and_replace(&Message { + tags: None, + prefix: None, + command: Command::PRIVMSG( + "#channel".to_string(), + "s/(will).*(be)/$2 $1/".to_string(), + ), + }) + .unwrap(), + "<user> this is a long message which be will replaced" + ) + } + + #[bench] + fn bench_replace(b: &mut Bencher) { + populate_log(); + b.iter(|| { + find_and_replace(&Message { + tags: None, + prefix: None, + command: Command::PRIVMSG( + "#channel".to_string(), + "s/will be/has been/".to_string(), + ), + }) + }); + } + + #[bench] + fn bench_replace_complex(b: &mut Bencher) { + populate_log(); + b.iter(|| { + find_and_replace(&Message { + tags: None, + prefix: None, + command: Command::PRIVMSG( + "#channel".to_string(), + "s/will be/has been/".to_string(), + ), + }) + }); + } +} diff --git a/src/hooks/shifty_eyes.rs b/src/hooks/shifty_eyes.rs new file mode 100644 index 0000000..beb3a9e --- /dev/null +++ b/src/hooks/shifty_eyes.rs @@ -0,0 +1,46 @@ +use anyhow::{anyhow, Result}; +use irc::client::prelude::*; + +const EYES: [char; 10] = ['^', 'v', 'V', '>', '<', 'x', 'X', '-', 'o', 'O']; +const NOSE: [char; 7] = ['.', '_', '-', ';', '\'', '"', '~']; + +pub fn shifty_eyes(bot: &crate::Bot, msg: Message) -> Result<()> { + if let Command::PRIVMSG(_, text) = msg.command.clone() { + if text.len() == 3 { + let mut chars = text.chars(); + let mut left = chars.next().unwrap(); + let middle = chars.next().unwrap(); + let mut right = chars.next().unwrap(); + + if EYES.contains(&left) && NOSE.contains(&middle) && EYES.contains(&right) { + left = invert(left)?; + right = invert(right)?; + + let mut result = String::new(); + result.push(left); + result.push(middle); + result.push(right); + + bot.send_privmsg(msg.response_target().unwrap(), result.as_str())?; + } + } + } + + Ok(()) +} + +fn invert(input: char) -> Result<char> { + match input { + '^' => Ok('v'), + 'v' => Ok('^'), + 'V' => Ok('^'), + '>' => Ok('<'), + '<' => Ok('>'), + 'x' => Ok('o'), + 'X' => Ok('O'), + '-' => Ok('o'), + 'o' => Ok('-'), + 'O' => Ok('-'), + _ => Err(anyhow!("not a valid char")), + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1e3be91 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,70 @@ +#![feature(test)] +extern crate test; + +use anyhow::Result; + +use irc::client::prelude::*; + +pub mod config; +pub mod hooks; + +pub use macros::catinator; + +#[macro_export] +macro_rules! reply { + ( $msg:expr, $text:expr ) => { + bot.send_privmsg($msg.response_target().unwrap(), $text.as_str())?; + }; +} + + +pub struct Bot { + pub config: config::Config, + pub irc_client: irc::client::Client, +} + +impl Bot { + pub async fn new(config_path: &str) -> Result<Bot> { + use std::fs; + + let config_str = fs::read_to_string(config_path)?; + let config: config::Config = toml::from_str(&config_str)?; + + let irc_config: Config = config.clone().into(); + + let irc_client = Client::from_config(irc_config.clone()).await?; + + Ok(Bot { irc_client, config }) + } + + pub async fn sasl_init(&self) -> Result<()> { + self.irc_client + .send_cap_req(&vec![irc::client::prelude::Capability::Sasl])?; + self.irc_client + .send(Command::NICK(self.config.user.nickname.clone()))?; + self.irc_client.send(Command::USER( + self.config.user.nickname.clone(), + "0".to_owned(), + self.config.user.realname.clone(), + ))?; + self.irc_client.send_sasl_plain()?; + + Ok(()) + } + + pub fn send_privmsg( + &self, + target: &str, + message: &str, + ) -> std::result::Result<(), irc::error::Error> { + self.irc_client.send_privmsg(target, message) + } + + pub fn send_notice( + &self, + target: &str, + message: &str, + ) -> std::result::Result<(), irc::error::Error> { + self.irc_client.send_notice(target, message) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..10082c3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,38 @@ +#[tokio::main] +async fn main() { + use catinator::catinator; + + tracing_subscriber::fmt() + .compact() + .with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL) + .with_max_level(tracing::Level::DEBUG) + .with_thread_ids(true) + .init(); + + catinator!( + hook( + "sasl", + "Handle Authentication.", + AUTHENTICATE, + catinator::hooks::sasl + ), + hook( + "sed_log", + "Log messages for use with sed replace, max 10k lines.", + PRIVMSG, + catinator::hooks::sed::log + ), + matcher( + "shifty_eyes", + ">.>", + r"^\S{3}$", + catinator::hooks::shifty_eyes + ), + matcher( + "replace", + "sed style replace with regex support. i/g/U/x sed flags available", + r"^s/", + catinator::hooks::sed::replace + ), + ); +} |
