Port Leek and Waifu command to new model (incl. bonus: rustfmt)

This commit is contained in:
lemonsh 2022-07-16 18:33:43 +02:00
parent 87ff04df03
commit 287d2657f3
9 changed files with 145 additions and 82 deletions

View file

@ -1,7 +1,7 @@
use std::collections::HashMap;
use fancy_regex::Regex;
use crate::ExecutorConnection; use crate::ExecutorConnection;
use async_trait::async_trait; use async_trait::async_trait;
use fancy_regex::{Captures, Regex};
use std::collections::HashMap;
fn separate_to_space(str: &str, prefix_len: usize) -> (&str, Option<&str>) { fn separate_to_space(str: &str, prefix_len: usize) -> (&str, Option<&str>) {
if let Some(o) = str.find(' ') { if let Some(o) = str.find(' ') {
@ -11,50 +11,58 @@ fn separate_to_space(str: &str, prefix_len: usize) -> (&str, Option<&str>) {
} }
} }
pub trait RegexCommand { #[async_trait]
fn execute(&mut self, message: String) -> anyhow::Result<String>; pub trait Trigger {
async fn execute(&mut self, msg: Message, matches: Captures) -> anyhow::Result<String>;
} }
#[async_trait] #[async_trait]
pub trait NormalCommand { pub trait Command {
async fn execute(&mut self, last_msg: &HashMap<String, String>, message: String) -> anyhow::Result<String>; //noinspection RsNeedlessLifetimes
async fn execute<'a>(&mut self, msg: Message<'a>) -> anyhow::Result<String>;
} }
#[derive(Default)] pub struct Message<'a> {
struct Commands { pub last_msg: &'a HashMap<String, String>,
regex: Vec<(Regex, Box<dyn RegexCommand + Send>)>, pub author: &'a str,
normal: HashMap<String, Box<dyn NormalCommand + Send>>, pub content: Option<&'a str>,
} }
pub struct Bot<SF: FnMut(String, String) -> anyhow::Result<()>> { pub struct Bot<SF: FnMut(String, String) -> anyhow::Result<()>> {
last_msg: HashMap<String, String>, last_msg: HashMap<String, String>,
prefix: String, prefix: String,
db: ExecutorConnection, db: ExecutorConnection,
commands: Commands, commands: HashMap<String, Box<dyn Command + Send>>,
sendmsg: SF triggers: Vec<(Regex, Box<dyn Trigger + Send>)>,
sendmsg: SF,
} }
impl<SF: FnMut(String, String) -> anyhow::Result<()>> Bot<SF> { impl<SF: FnMut(String, String) -> anyhow::Result<()>> Bot<SF> {
pub fn new(prefix: String, db: ExecutorConnection, sendmsg: SF) -> Self { pub fn new(prefix: String, db: ExecutorConnection, sendmsg: SF) -> Self {
Bot { Bot {
last_msg: HashMap::new(), last_msg: HashMap::new(),
commands: HashMap::new(),
triggers: Vec::new(),
prefix, prefix,
db, db,
commands: Commands::default(), sendmsg,
sendmsg
} }
} }
pub fn add_command<C: NormalCommand + Send + 'static>(&mut self, name: String, cmd: C) { pub fn add_command<C: Command + Send + 'static>(&mut self, name: String, cmd: C) {
self.commands.normal.insert(name, Box::new(cmd)); self.commands.insert(name, Box::new(cmd));
} }
pub fn add_regex_command<C: RegexCommand + Send + 'static>(&mut self, regex: Regex, cmd: C) { pub fn add_regex_command<C: Trigger + Send + 'static>(&mut self, regex: Regex, cmd: C) {
self.commands.regex.push((regex, Box::new(cmd))); self.triggers.push((regex, Box::new(cmd)));
} }
pub async fn handle_message(&mut self, origin: &str, author: &str, content: &str) -> anyhow::Result<()> { pub async fn handle_message(
(self.sendmsg)(origin.into(), content.into()).unwrap(); &mut self,
origin: &str,
author: &str,
content: &str,
) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
} }

View file

@ -1,4 +0,0 @@
pub mod leek;
pub mod misc;
pub mod sed;
pub mod title;

20
src/commands/help.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::bot::{Message, Command};
use async_trait::async_trait;
const HELP: &str = concat!(
"=- \x1d\x02Überbot\x0f ", env!("CARGO_PKG_VERSION"), " -=\n",
" * waifu <category>\n",
" * owo/mock/leet [user]\n",
" * ev <math expression>\n",
" - This bot also provides titles of URLs and details for Spotify URIs/links. It can also resolve sed expressions.\n"
);
pub struct Help;
#[async_trait]
impl Command for Help {
//noinspection RsNeedlessLifetimes
async fn execute<'a>(&mut self, _msg: Message<'a>) -> anyhow::Result<String> {
Ok(HELP.into())
}
}

View file

@ -1,4 +1,6 @@
use crate::bot::{Command, Message};
use arrayvec::ArrayString; use arrayvec::ArrayString;
use async_trait::async_trait;
use rand::Rng; use rand::Rng;
use std::{ use std::{
error::Error, error::Error,
@ -85,11 +87,11 @@ fn owoify(input: &str) -> LeekResult {
// textmoji // textmoji
'.' => { '.' => {
builder.try_push_str(match rng.gen_range(0..6) { builder.try_push_str(match rng.gen_range(0..6) {
1 => " OwO", 1 => " >~<",
2 => " (◕ᴗ◕✿)", 2 => " (◕ᴗ◕✿)",
3 => " >w<", 3 => " >w<",
4 => " >_<", 4 => " >_<",
5 => " ^•ﻌ•^", 5 => " OwO",
_ => " ^^", _ => " ^^",
})?; })?;
} }
@ -103,33 +105,49 @@ fn owoify(input: &str) -> LeekResult {
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum Command { enum LeekCommand {
Owo, Owo,
Leet, Leet,
Mock, Mock,
} }
pub fn execute( fn execute_leek(cmd: LeekCommand, msg: &Message) -> anyhow::Result<String> {
state: &mut crate::AppState, let nick = msg.content.unwrap_or(msg.author);
cmd: Command, match msg.last_msg.get(nick) {
target: &str, Some(msg) => Ok(match cmd {
nick: &str, LeekCommand::Owo => owoify(msg)?,
) -> anyhow::Result<()> { LeekCommand::Leet => leetify(msg),
match state.last_msgs.get(nick) { LeekCommand::Mock => mock(msg),
Some(msg) => {
tracing::debug!("Executing {:?} on {:?}", cmd, msg);
let output = match cmd {
Command::Owo => super::leek::owoify(msg)?,
Command::Leet => super::leek::leetify(msg),
Command::Mock => super::leek::mock(msg),
};
state.client.send_privmsg(target, &output)?;
}
None => {
state
.client
.send_privmsg(target, "No last messages found.")?;
} }
.to_string()),
None => Ok("No previous messages found.".into()),
}
}
pub struct Owo;
pub struct Leet;
pub struct Mock;
#[async_trait]
impl Command for Owo {
//noinspection RsNeedlessLifetimes
async fn execute<'a>(&mut self, msg: Message<'a>) -> anyhow::Result<String> {
execute_leek(LeekCommand::Owo, &msg)
}
}
#[async_trait]
impl Command for Leet {
//noinspection RsNeedlessLifetimes
async fn execute<'a>(&mut self, msg: Message<'a>) -> anyhow::Result<String> {
execute_leek(LeekCommand::Leet, &msg)
}
}
#[async_trait]
impl Command for Mock {
//noinspection RsNeedlessLifetimes
async fn execute<'a>(&mut self, msg: Message<'a>) -> anyhow::Result<String> {
execute_leek(LeekCommand::Mock, &msg)
} }
Ok(())
} }

3
src/commands/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod help;
pub mod leek;
pub mod waifu;

24
src/commands/waifu.rs Normal file
View file

@ -0,0 +1,24 @@
use crate::bot::{Message, Command};
use async_trait::async_trait;
use serde_json::Value;
pub struct Waifu;
#[async_trait]
impl Command for Waifu {
//noinspection RsNeedlessLifetimes
async fn execute<'a>(&mut self, msg: Message<'a>) -> anyhow::Result<String> {
let category = msg.content.unwrap_or("waifu");
let api_resp = reqwest::get(format!("https://api.waifu.pics/sfw/{}", category))
.await?
.text()
.await?;
let api_resp = api_resp.trim();
let value: Value = serde_json::from_str(api_resp)?;
let url = value["url"]
.as_str()
.unwrap_or("Invalid API Response.")
.to_string();
Ok(url)
}
}

View file

@ -1,4 +1,3 @@
use std::net::SocketAddr;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -24,4 +23,4 @@ pub struct IrcConfig {
pub port: u16, pub port: u16,
pub username: String, pub username: String,
pub prefix: String, pub prefix: String,
} }

View file

@ -135,6 +135,16 @@ impl ExecutorConnection {
Option<Quote>, Option<Quote>,
author: Option<String> author: Option<String>
); );
executor_wrapper!(search_quotes, Task::SearchQuotes, Option<Vec<Quote>>, query: String); executor_wrapper!(
executor_wrapper!(random_n_quotes, Task::RandomNQuotes, Option<Vec<Quote>>, count: u8); search_quotes,
Task::SearchQuotes,
Option<Vec<Quote>>,
query: String
);
executor_wrapper!(
random_n_quotes,
Task::RandomNQuotes,
Option<Vec<Quote>>,
count: u8
);
} }

View file

@ -1,41 +1,31 @@
#![allow(clippy::match_wildcard_for_single_variants)] #![allow(clippy::match_wildcard_for_single_variants)]
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 std::env;
use std::fmt::Display;
use crate::bot::Bot;
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;
use irc::client::{Client, ClientStream}; use irc::client::{Client, ClientStream};
use irc::proto::{ChannelExt, Command, Prefix}; use irc::proto::{ChannelExt, Command, Prefix};
use rspotify::Credentials; use rspotify::Credentials;
use serde::Deserialize;
use tokio::select; 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::bot::Bot; use crate::commands::leek::Owo;
use crate::bots::misc::Waifu;
use crate::config::UberConfig; use crate::config::UberConfig;
use crate::database::{DbExecutor, ExecutorConnection}; use crate::database::{DbExecutor, ExecutorConnection};
mod bots;
mod database;
mod bot; mod bot;
mod commands;
mod config; mod config;
mod database;
// this will be displayed when the help command is used
const HELP: &[&str] = &[
concat!("=- \x1d\x02Ü\x02berbot\x0f ", env!("CARGO_PKG_VERSION"), " -="),
" * waifu <category>",
" * owo/mock/leet [user]",
" * ev <math expression>",
" - This bot also provides titles of URLs and details for Spotify URIs/links. It can also resolve sed expressions."
];
#[cfg(unix)] #[cfg(unix)]
async fn terminate_signal() { async fn terminate_signal() {
@ -60,7 +50,7 @@ async fn terminate_signal() {
pub struct AppState<SF: FnMut(String, String) -> anyhow::Result<()>> { pub struct AppState<SF: FnMut(String, String) -> anyhow::Result<()>> {
client: Arc<Client>, client: Arc<Client>,
stream: ClientStream, stream: ClientStream,
bot: Bot<SF> bot: Bot<SF>,
} }
#[tokio::main] #[tokio::main]
@ -76,8 +66,7 @@ async fn main() -> anyhow::Result<()> {
let cfg: UberConfig = toml::from_str(&client_conf)?; let cfg: UberConfig = toml::from_str(&client_conf)?;
let (db_exec, db_conn) = let (db_exec, db_conn) = DbExecutor::create(cfg.db_path.as_deref().unwrap_or("uberbot.db3"))?;
DbExecutor::create(cfg.db_path.as_deref().unwrap_or("uberbot.db3"))?;
let exec_thread = thread::spawn(move || { let exec_thread = thread::spawn(move || {
db_exec.run(); db_exec.run();
tracing::info!("Database executor has been shut down"); tracing::info!("Database executor has been shut down");
@ -85,12 +74,7 @@ async fn main() -> anyhow::Result<()> {
let uber_ver = concat!("Überbot ", env!("CARGO_PKG_VERSION")); let uber_ver = concat!("Überbot ", env!("CARGO_PKG_VERSION"));
let irc_config = Config { let irc_config = Config {
nickname: Some( nickname: Some(cfg.irc.nickname.unwrap_or_else(|| cfg.irc.username.clone())),
cfg
.irc
.nickname
.unwrap_or_else(|| cfg.irc.username.clone()),
),
username: Some(cfg.irc.username.clone()), username: Some(cfg.irc.username.clone()),
realname: Some(cfg.irc.username), realname: Some(cfg.irc.username),
server: Some(cfg.irc.host), server: Some(cfg.irc.host),
@ -116,11 +100,12 @@ async fn main() -> anyhow::Result<()> {
}); });
bot.add_command("waifu".into(), Waifu); bot.add_command("waifu".into(), Waifu);
bot.add_command("owo".into(), Owo);
let state = AppState { let state = AppState {
client: client.clone(), client: client.clone(),
stream, stream,
bot bot,
}; };
let message_loop_task = tokio::spawn(async move { let message_loop_task = tokio::spawn(async move {
if let Err(e) = message_loop(state).await { if let Err(e) = message_loop(state).await {
@ -157,7 +142,9 @@ async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
async fn message_loop<SF: FnMut(String, String) -> anyhow::Result<()>>(mut state: AppState<SF>) -> anyhow::Result<()> { async fn message_loop<SF: FnMut(String, String) -> anyhow::Result<()>>(
mut state: AppState<SF>,
) -> anyhow::Result<()> {
while let Some(message) = state.stream.next().await.transpose()? { while let Some(message) = state.stream.next().await.transpose()? {
if let Command::PRIVMSG(ref origin, content) = message.command { if let Command::PRIVMSG(ref origin, content) = message.command {
if origin.is_channel_name() { if origin.is_channel_name() {
@ -178,5 +165,3 @@ async fn message_loop<SF: FnMut(String, String) -> anyhow::Result<()>>(mut state
} }
Ok(()) Ok(())
} }