aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMax Audron <audron@cocaine.farm>2021-05-15 13:58:01 +0200
committerMax Audron <audron@cocaine.farm>2021-05-15 13:58:01 +0200
commitd0bff910b0b038ee85bc285bef7a63870a3474ab (patch)
treeb840d0b932c080fa5d5053b6bf66a91f94a80481 /src
init
Diffstat (limited to 'src')
-rw-r--r--src/config.rs46
-rw-r--r--src/hooks/mod.rs39
-rw-r--r--src/hooks/sed.rs175
-rw-r--r--src/hooks/shifty_eyes.rs46
-rw-r--r--src/lib.rs70
-rw-r--r--src/main.rs38
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
+ ),
+ );
+}