aboutsummaryrefslogtreecommitdiff
path: root/src/hooks/sed/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/hooks/sed/mod.rs')
-rw-r--r--src/hooks/sed/mod.rs252
1 files changed, 252 insertions, 0 deletions
diff --git a/src/hooks/sed/mod.rs b/src/hooks/sed/mod.rs
new file mode 100644
index 0000000..3128372
--- /dev/null
+++ b/src/hooks/sed/mod.rs
@@ -0,0 +1,252 @@
+use anyhow::{anyhow, bail, Context, Result};
+use irc::client::prelude::*;
+
+use std::collections::HashMap;
+
+#[allow(dead_code)]
+mod parser;
+
+static LOG_MAX_SIZE: usize = 10000;
+
+thread_local!(static RE: regex::Regex = regex::Regex::new(r"^s/").unwrap());
+
+pub struct Sed(HashMap<String, Vec<(String, String)>>);
+
+impl Sed {
+ pub fn new() -> Sed {
+ Sed(HashMap::new())
+ }
+
+ pub fn log(&mut self, _bot: &crate::Bot, msg: Message) -> Result<()> {
+ self.log_msg(msg).context("failed to log new message")
+ }
+
+ fn log_msg(&mut self, msg: Message) -> Result<()> {
+ if let Command::PRIVMSG(target, mut text) = msg.command.clone() {
+ if text.starts_with("\x01ACTION") {
+ text = text.replace("\x01ACTION", "\x01\x01");
+ }
+
+ match self.0.get_mut(&target) {
+ Some(log) => {
+ if log.len() >= LOG_MAX_SIZE {
+ let _ = log.remove(0);
+ }
+ log.push((msg.source_nickname().unwrap().to_string(), text))
+ }
+ None => {
+ let mut log = Vec::with_capacity(LOG_MAX_SIZE);
+ log.push((msg.source_nickname().unwrap().to_string(), text));
+ self.0.insert(target, log);
+ }
+ }
+ }
+ Ok(())
+ }
+
+ pub fn replace(&mut self, bot: &crate::Bot, msg: Message) -> Result<()> {
+ match self.find_and_replace(&msg) {
+ Ok(res) => match bot.send_privmsg(msg.response_target().unwrap(), res.as_str()) {
+ Ok(_) => Ok(()),
+ Err(_) => bail!(
+ "failed to send message: \"{:?}\" to channel: {:?}",
+ msg.response_target().unwrap(),
+ res
+ ),
+ },
+ Err(_) => bail!("did not find match for: {:?}", msg),
+ }
+ }
+
+ fn find_and_replace(&mut self, msg: &Message) -> Result<String> {
+ if let Command::PRIVMSG(target, text) = msg.command.clone() {
+ let cmd =
+ parser::Command::from_str(text.as_str()).context("failed to parse sed command")?;
+
+ let log = self
+ .0
+ .get(&target)
+ .context("did not find log for current channel")?;
+
+ return log
+ .iter()
+ .rev()
+ .find(|(_, text)| cmd.regex().is_match(text) && !RE.with(|re| re.is_match(text)))
+ .and_then(|(nick, text)| {
+ if text.starts_with("\x01\x01") {
+ Some(format!(
+ "* {}{}",
+ nick,
+ cmd.execute(&text.replace("\x01", ""))
+ ))
+ } else {
+ 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::*;
+
+ pub fn populate_log() -> Sed {
+ let mut sed = Sed::new();
+
+ sed.log_msg(
+ Message::new(
+ Some("user!user@user.com"),
+ "PRIVMSG",
+ vec!["user", "this is a long message which will be replaced"],
+ )
+ .unwrap(),
+ )
+ .unwrap();
+
+ for _ in 0..LOG_MAX_SIZE - 1 {
+ sed.log_msg(
+ Message::new(
+ Some("user!user@user.com"),
+ "PRIVMSG",
+ vec!["user", "this is a long message which doesn't matter"],
+ )
+ .unwrap(),
+ )
+ .unwrap();
+ }
+
+ return sed;
+ }
+
+ #[test]
+ fn test_log_push_max() {
+ let mut sed = Sed::new();
+
+ sed.log_msg(
+ Message::new(Some("user!user@user.com"), "PRIVMSG", vec!["user", "one"]).unwrap(),
+ )
+ .unwrap();
+
+ for _ in 0..LOG_MAX_SIZE - 2 {
+ sed.log_msg(
+ Message::new(Some("user!user@user.com"), "PRIVMSG", vec!["user", "two"]).unwrap(),
+ )
+ .unwrap();
+ }
+ sed.log_msg(
+ Message::new(Some("user!user@user.com"), "PRIVMSG", vec!["user", "three"]).unwrap(),
+ )
+ .unwrap();
+
+ {
+ let log = sed.0.get("user").unwrap();
+ assert_eq!(
+ log[LOG_MAX_SIZE - 1],
+ ("user".to_string(), "three".to_string())
+ );
+ assert_eq!(log[0], ("user".to_string(), "one".to_string()));
+ }
+
+ sed.log_msg(
+ Message::new(Some("user!user@user.com"), "PRIVMSG", vec!["user", "four"]).unwrap(),
+ )
+ .unwrap();
+
+ {
+ let log = sed.0.get("user").unwrap();
+
+ assert_eq!(
+ log[LOG_MAX_SIZE - 1],
+ ("user".to_string(), "four".to_string())
+ );
+ assert_eq!(log[0], ("user".to_string(), "two".to_string()));
+ }
+ }
+
+ #[test]
+ fn test_log_limit() {
+ let mut sed = populate_log();
+
+ {
+ let log = sed.0.get("user").unwrap();
+ assert_eq!(log.len(), LOG_MAX_SIZE);
+ }
+
+ sed.log_msg(
+ Message::new(
+ Some("user!user@user.com"),
+ "PRIVMSG",
+ vec!["user", "this is the 10001th message"],
+ )
+ .unwrap(),
+ )
+ .unwrap();
+
+ {
+ let log = sed.0.get("user").unwrap();
+ assert_eq!(log.len(), LOG_MAX_SIZE);
+ }
+ }
+
+ #[test]
+ fn test_replace() {
+ let mut sed = populate_log();
+ assert_eq!(
+ sed.find_and_replace(&Message {
+ tags: None,
+ prefix: None,
+ command: Command::PRIVMSG("user".to_string(), "s/will be/has been/".to_string(),),
+ })
+ .unwrap(),
+ "<user> this is a long message which \x02has been\x02 replaced"
+ )
+ }
+
+ #[test]
+ fn test_replace_complex() {
+ let mut sed = populate_log();
+ assert_eq!(
+ sed.find_and_replace(&Message {
+ tags: None,
+ prefix: None,
+ command: Command::PRIVMSG("user".to_string(), "s/(will).*(be)/$2 $1/".to_string(),),
+ })
+ .unwrap(),
+ "<user> this is a long message which \x02be will\x02 replaced"
+ )
+ }
+}
+
+#[cfg(all(test, feature = "bench"))]
+mod bench {
+ use super::*;
+ use test::Bencher;
+
+ #[bench]
+ fn bench_replace(b: &mut Bencher) {
+ let mut sed = tests::populate_log();
+ b.iter(|| {
+ sed.find_and_replace(&Message {
+ tags: None,
+ prefix: None,
+ command: Command::PRIVMSG("user".to_string(), "s/will be/has been/".to_string()),
+ })
+ });
+ }
+
+ #[bench]
+ fn bench_replace_complex(b: &mut Bencher) {
+ let mut sed = tests::populate_log();
+ b.iter(|| {
+ sed.find_and_replace(&Message {
+ tags: None,
+ prefix: None,
+ command: Command::PRIVMSG("user".to_string(), "s/(will).*(be)/$2 $1/".to_string()),
+ })
+ });
+ }
+}