Implement quotes

This commit is contained in:
lemonsh 2022-07-17 20:10:34 +02:00
parent 3eaaeb1244
commit 25a7635408
9 changed files with 153 additions and 61 deletions

View file

@ -1,11 +1,12 @@
use crate::history::MessageHistory;
use crate::ExecutorConnection; use crate::ExecutorConnection;
use async_trait::async_trait; use async_trait::async_trait;
use fancy_regex::{Captures, Regex}; use fancy_regex::{Captures, Regex};
use std::collections::HashMap; use std::collections::HashMap;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::Mutex;
fn dissect<'a>(prefix: &str, str: &'a str) -> Option<(&'a str, Option<&'a str>)> { fn dissect<'a>(prefix: &str, str: &'a str) -> Option<(&'a str, Option<&'a str>)> {
let str = str.strip_prefix(prefix)?; let str = str.trim().strip_prefix(prefix)?;
if let Some(o) = str.find(' ') { if let Some(o) = str.find(' ') {
Some((&str[..o], Some(&str[o + 1..]))) Some((&str[..o], Some(&str[o + 1..])))
} else { } else {
@ -15,7 +16,11 @@ fn dissect<'a>(prefix: &str, str: &'a str) -> Option<(&'a str, Option<&'a str>)>
#[async_trait] #[async_trait]
pub trait Trigger { pub trait Trigger {
async fn execute<'a>(&mut self, msg: Context<'a>, captures: Captures<'a>) -> anyhow::Result<String>; async fn execute<'a>(
&mut self,
msg: Context<'a>,
captures: Captures<'a>,
) -> anyhow::Result<String>;
} }
#[async_trait] #[async_trait]
@ -24,15 +29,15 @@ pub trait Command {
} }
pub struct Context<'a> { pub struct Context<'a> {
pub last_msg: &'a RwLock<HashMap<String, String>>, pub history: &'a MessageHistory,
pub author: &'a str, pub author: &'a str,
// in case of triggers, this is always Some(...) // in case of triggers, this is always Some(...)
pub content: Option<&'a str>, pub content: Option<&'a str>,
pub db: &'a ExecutorConnection pub db: &'a ExecutorConnection,
} }
pub struct Bot<SF: Fn(String, String) -> anyhow::Result<()>> { pub struct Bot<SF: Fn(String, String) -> anyhow::Result<()>> {
last_msg: RwLock<HashMap<String, String>>, history: MessageHistory,
prefix: String, prefix: String,
db: ExecutorConnection, db: ExecutorConnection,
commands: HashMap<String, Box<Mutex<dyn Command + Send>>>, commands: HashMap<String, Box<Mutex<dyn Command + Send>>>,
@ -41,9 +46,9 @@ pub struct Bot<SF: Fn(String, String) -> anyhow::Result<()>> {
} }
impl<SF: Fn(String, String) -> anyhow::Result<()>> Bot<SF> { impl<SF: Fn(String, String) -> anyhow::Result<()>> Bot<SF> {
pub fn new(prefix: String, db: ExecutorConnection, sendmsg: SF) -> Self { pub fn new(prefix: String, db: ExecutorConnection, hdepth: usize, sendmsg: SF) -> Self {
Bot { Bot {
last_msg: RwLock::new(HashMap::new()), history: MessageHistory::new(hdepth),
commands: HashMap::new(), commands: HashMap::new(),
triggers: Vec::new(), triggers: Vec::new(),
prefix, prefix,
@ -69,38 +74,36 @@ impl<SF: Fn(String, String) -> anyhow::Result<()>> Bot<SF> {
if let Some((command, remainder)) = dissect(&self.prefix, content) { if let Some((command, remainder)) = dissect(&self.prefix, content) {
if let Some(handler) = self.commands.get(command) { if let Some(handler) = self.commands.get(command) {
let msg = Context { let msg = Context {
last_msg: &self.last_msg,
author, author,
content: remainder, content: remainder,
db: &self.db db: &self.db,
history: &self.history,
}; };
return (self.sendmsg)(origin.into(), handler.lock().await.execute(msg).await?) return (self.sendmsg)(origin.into(), handler.lock().await.execute(msg).await?);
} }
return (self.sendmsg)(origin.into(), "Unknown command.".into()) return (self.sendmsg)(origin.into(), "Unknown command.".into());
} else { } else {
for trigger in &self.triggers { for trigger in &self.triggers {
let captures = trigger.0.captures(content)?; let captures = trigger.0.captures(content)?;
if let Some(captures) = captures { if let Some(captures) = captures {
let msg = Context { let msg = Context {
last_msg: &self.last_msg,
author, author,
content: Some(content), content: Some(content),
db: &self.db db: &self.db,
history: &self.history,
}; };
return (self.sendmsg)(origin.into(), trigger.1.lock().await.execute(msg, captures).await?) return (self.sendmsg)(
origin.into(),
trigger.1.lock().await.execute(msg, captures).await?,
);
} }
} }
self.last_msg.write().await.insert(author.to_string(), content.to_string()); self.history.add_message(author, content).await;
} }
Ok(()) Ok(())
} }
pub async fn handle_message( pub async fn handle_message(&self, origin: &str, author: &str, content: &str) {
&self,
origin: &str,
author: &str,
content: &str,
) {
if let Err(e) = self.handle_message_inner(origin, author, content).await { if let Err(e) = self.handle_message_inner(origin, author, content).await {
let _ = (self.sendmsg)(origin.into(), format!("Error: {}", e)); let _ = (self.sendmsg)(origin.into(), format!("Error: {}", e));
} }

View file

@ -1,10 +1,10 @@
use std::collections::HashMap;
use async_trait::async_trait;
use crate::bot::{Command, Context}; use crate::bot::{Command, Context};
use async_trait::async_trait;
use std::collections::HashMap;
#[derive(Default)] #[derive(Default)]
pub struct Eval { pub struct Eval {
last_eval: HashMap<String, f64> last_eval: HashMap<String, f64>,
} }
#[async_trait] #[async_trait]

View file

@ -1,4 +1,4 @@
use crate::bot::{Context, Command}; use crate::bot::{Command, Context};
use async_trait::async_trait; use async_trait::async_trait;
const HELP: &str = concat!( const HELP: &str = concat!(

View file

@ -113,11 +113,11 @@ enum LeekCommand {
async fn execute_leek(cmd: LeekCommand, msg: &Context<'_>) -> anyhow::Result<String> { async fn execute_leek(cmd: LeekCommand, msg: &Context<'_>) -> anyhow::Result<String> {
let nick = msg.content.unwrap_or(msg.author); let nick = msg.content.unwrap_or(msg.author);
match msg.last_msg.read().await.get(nick) { match msg.history.last_msg(nick).await {
Some(msg) => Ok(match cmd { Some(msg) => Ok(match cmd {
LeekCommand::Owo => owoify(msg)?, LeekCommand::Owo => owoify(&msg)?,
LeekCommand::Leet => leetify(msg), LeekCommand::Leet => leetify(&msg),
LeekCommand::Mock => mock(msg), LeekCommand::Mock => mock(&msg),
} }
.to_string()), .to_string()),
None => Ok("No previous messages found.".into()), None => Ok("No previous messages found.".into()),

View file

@ -1,10 +1,11 @@
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
pub mod debug; pub mod debug;
pub mod eval;
pub mod help; pub mod help;
pub mod leek; pub mod leek;
pub mod waifu; pub mod quotes;
pub mod sed; pub mod sed;
pub mod eval;
pub mod spotify; pub mod spotify;
pub mod title; pub mod title;
pub mod waifu;

58
src/commands/quotes.rs Normal file
View file

@ -0,0 +1,58 @@
use crate::bot::{Command, Context};
use crate::database::Quote;
use async_trait::async_trait;
pub struct Grab;
pub struct Quot;
#[async_trait]
impl Command for Grab {
async fn execute(&mut self, msg: Context<'_>) -> anyhow::Result<String> {
let content = if let Some(c) = msg.content {
c
} else {
return Ok("Invalid usage.".into());
};
let mut split = content.splitn(2, ' ');
let split = (split.next().unwrap(), split.next());
let (author, count) = if let Some(author) = split.1 {
(author, split.0.parse::<usize>()?)
} else {
(split.0, 1)
};
if count == 0 {
return Ok("So are you going to grab anything?".into());
}
if author == msg.author {
return Ok("You can't grab yourself.".into());
}
let message = msg
.history
.last_msgs(author, count)
.await
.map(|v| v.join(" | "));
if let Some(message) = message {
msg.db
.add_quote(Quote {
author: author.into(),
quote: message,
})
.await?;
Ok("Quote added ({} messages).".into())
} else {
Ok("No previous messages to grab.".into())
}
}
}
#[async_trait]
impl Command for Quot {
async fn execute(&mut self, msg: Context<'_>) -> anyhow::Result<String> {
let author = msg.content.map(ToString::to_string);
if let Some(q) = msg.db.get_quote(author).await? {
Ok(format!("\"{}\" ~{}", q.quote, q.author))
} else {
Ok("No quotes found from this user.".into())
}
}
}

View file

@ -1,12 +1,16 @@
use crate::bot::{Context, Trigger};
use async_trait::async_trait; use async_trait::async_trait;
use fancy_regex::Captures; use fancy_regex::Captures;
use crate::bot::{Context, Trigger};
pub struct Sed; pub struct Sed;
#[async_trait] #[async_trait]
impl Trigger for Sed { impl Trigger for Sed {
async fn execute<'a>(&mut self, msg: Context<'a>, captures: Captures<'a>) -> anyhow::Result<String> { async fn execute<'a>(
&mut self,
msg: Context<'a>,
captures: Captures<'a>,
) -> anyhow::Result<String> {
let foreign_author; let foreign_author;
let author = if let Some(author) = captures.name("u").map(|m| m.as_str()) { let author = if let Some(author) = captures.name("u").map(|m| m.as_str()) {
foreign_author = true; foreign_author = true;
@ -15,8 +19,7 @@ impl Trigger for Sed {
foreign_author = false; foreign_author = false;
msg.author msg.author
}; };
let lastmsg = msg.last_msg.read().await; let message = if let Some(msg) = msg.history.last_msg(author).await {
let message = if let Some(msg) = lastmsg.get(author) {
msg msg
} else { } else {
return Ok("No previous messages found.".into()); return Ok("No previous messages found.".into());
@ -25,11 +28,15 @@ impl Trigger for Sed {
// TODO: karx plz add flags // TODO: karx plz add flags
//let flags = matches.name("f").map(|m| m.as_str()); //let flags = matches.name("f").map(|m| m.as_str());
let result = message.replace(find.as_str(), replace.as_str()); let result = message.replace(find.as_str(), replace.as_str());
drop(lastmsg);
if foreign_author { if foreign_author {
Ok(format!("(edited by {}) <{}> {}", msg.author, author, result)) Ok(format!(
"(edited by {}) <{}> {}",
msg.author, author, result
))
} else { } else {
msg.last_msg.write().await.insert(author.into(), result.to_string()); msg.history
.edit_message(author, 0, result.to_string())
.await;
Ok(format!("<{}> {}", author, result)) Ok(format!("<{}> {}", author, result))
} }
} else { } else {

View file

@ -1,24 +1,19 @@
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use anyhow::anyhow;
use tokio::sync::RwLock; use tokio::sync::RwLock;
pub struct MessageHistory { pub struct MessageHistory {
map: RwLock<HashMap<String, VecDeque<String>>>, map: RwLock<HashMap<String, VecDeque<String>>>,
maxlen: usize maxlen: usize,
} }
impl MessageHistory { impl MessageHistory {
pub fn new(maxlen: usize) -> MessageHistory { pub fn new(maxlen: usize) -> MessageHistory {
MessageHistory { MessageHistory {
map: RwLock::new(HashMap::new()), map: RwLock::new(HashMap::new()),
maxlen maxlen,
} }
} }
pub fn max_len(&self) -> usize {
self.maxlen
}
pub async fn last_msg(&self, user: &str) -> Option<String> { pub async fn last_msg(&self, user: &str) -> Option<String> {
let map = self.map.read().await; let map = self.map.read().await;
map.get(user).and_then(|d| d.get(0)).map(|s| s.to_string()) map.get(user).and_then(|d| d.get(0)).map(|s| s.to_string())
@ -27,18 +22,39 @@ impl MessageHistory {
pub async fn last_msgs(&self, user: &str, count: usize) -> Option<Vec<String>> { pub async fn last_msgs(&self, user: &str, count: usize) -> Option<Vec<String>> {
let map = self.map.read().await; let map = self.map.read().await;
if let Some(deque) = map.get(user) { if let Some(deque) = map.get(user) {
let count = if deque.len() < count { deque.len() } else {count}; let count = if deque.len() < count {
Some(deque.range(..count).rev().map(ToString::to_string).collect()) deque.len()
} else {
count
};
Some(
deque
.range(..count)
.rev()
.map(ToString::to_string)
.collect(),
)
} else { } else {
None None
} }
} }
pub async fn edit_message(&self, user: &str, pos: usize, edited: String) -> bool {
let mut map = self.map.write().await;
if let Some(deque) = map.get_mut(user) {
if let Some(old) = deque.get_mut(pos) {
*old = edited;
return true;
}
}
false
}
pub async fn add_message(&self, user: &str, message: &str) { pub async fn add_message(&self, user: &str, message: &str) {
let mut map = self.map.write().await; let mut map = self.map.write().await;
if let Some(deque) = map.get_mut(user) { if let Some(deque) = map.get_mut(user) {
if deque.len() == self.maxlen { if deque.len() == self.maxlen {
deque.remove(deque.len()-1); deque.remove(deque.len() - 1);
} }
deque.push_front(message.to_string()); deque.push_front(message.to_string());
} else { } else {
@ -47,4 +63,4 @@ impl MessageHistory {
map.insert(user.to_string(), deque); map.insert(user.to_string(), deque);
} }
} }
} }

View file

@ -1,11 +1,18 @@
use fancy_regex::Regex;
use std::env; use std::env;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use fancy_regex::Regex;
use crate::bot::Bot; use crate::bot::Bot;
use crate::commands::eval::Eval;
use crate::commands::help::Help;
use crate::commands::leek::Owo;
use crate::commands::quotes::{Grab, Quot};
use crate::commands::sed::Sed;
use crate::commands::spotify::Spotify;
use crate::commands::title::Title;
use crate::commands::waifu::Waifu; use crate::commands::waifu::Waifu;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use irc::client::prelude::Config; use irc::client::prelude::Config;
@ -16,12 +23,6 @@ use tokio::select;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::mpsc::unbounded_channel; use tokio::sync::mpsc::unbounded_channel;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use crate::commands::eval::Eval;
use crate::commands::help::Help;
use crate::commands::leek::Owo;
use crate::commands::sed::Sed;
use crate::commands::spotify::Spotify;
use crate::commands::title::Title;
use crate::config::UberConfig; use crate::config::UberConfig;
use crate::database::{DbExecutor, ExecutorConnection}; use crate::database::{DbExecutor, ExecutorConnection};
@ -30,6 +31,7 @@ mod bot;
mod commands; mod commands;
mod config; mod config;
mod database; mod database;
mod history;
#[cfg(unix)] #[cfg(unix)]
async fn terminate_signal() { async fn terminate_signal() {
@ -89,7 +91,7 @@ async fn main() -> anyhow::Result<()> {
let (ctx, _) = broadcast::channel(1); let (ctx, _) = broadcast::channel(1);
let (etx, mut erx) = unbounded_channel(); let (etx, mut erx) = unbounded_channel();
let mut bot = Bot::new(cfg.irc.prefix, db_conn, { let mut bot = Bot::new(cfg.irc.prefix, db_conn, 3, {
let client = client.clone(); let client = client.clone();
move |target, msg| Ok(client.send_privmsg(target, msg)?) move |target, msg| Ok(client.send_privmsg(target, msg)?)
}); });
@ -98,7 +100,12 @@ async fn main() -> anyhow::Result<()> {
bot.add_command("waifu".into(), Waifu::default()); bot.add_command("waifu".into(), Waifu::default());
bot.add_command("owo".into(), Owo); bot.add_command("owo".into(), Owo);
bot.add_command("ev".into(), Eval::default()); bot.add_command("ev".into(), Eval::default());
bot.add_trigger(Regex::new(r"^(?:(?<u>\S+):\s+)?s/(?<r>[^/]*)/(?<w>[^/]*)(?:/(?<f>[a-z]*))?\s*")?, Sed); bot.add_command("grab".into(), Grab);
bot.add_command("quot".into(), Quot);
bot.add_trigger(
Regex::new(r"^(?:(?<u>\S+):\s+)?s/(?<r>[^/]*)/(?<w>[^/]*)(?:/(?<f>[a-z]*))?\s*")?,
Sed,
);
if let Some(spotcfg) = cfg.spotify { if let Some(spotcfg) = cfg.spotify {
let creds = Credentials::new(&spotcfg.client_id, &spotcfg.client_secret); let creds = Credentials::new(&spotcfg.client_id, &spotcfg.client_secret);
let spotify = Spotify::new(creds).await?; let spotify = Spotify::new(creds).await?;
@ -150,7 +157,7 @@ async fn main() -> anyhow::Result<()> {
async fn message_loop<SF: Fn(String, String) -> anyhow::Result<()>>( async fn message_loop<SF: Fn(String, String) -> anyhow::Result<()>>(
mut stream: ClientStream, mut stream: ClientStream,
bot: Bot<SF> bot: Bot<SF>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
while let Some(message) = stream.next().await.transpose()? { while let Some(message) = stream.next().await.transpose()? {
if let Command::PRIVMSG(ref origin, content) = message.command { if let Command::PRIVMSG(ref origin, content) = message.command {