Port Leek and Waifu command to new model (incl. bonus: rustfmt)
This commit is contained in:
parent
87ff04df03
commit
287d2657f3
50
src/bot.rs
50
src/bot.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
pub mod leek;
|
|
||||||
pub mod misc;
|
|
||||||
pub mod sed;
|
|
||||||
pub mod title;
|
|
20
src/commands/help.rs
Normal file
20
src/commands/help.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
3
src/commands/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod help;
|
||||||
|
pub mod leek;
|
||||||
|
pub mod waifu;
|
24
src/commands/waifu.rs
Normal file
24
src/commands/waifu.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
43
src/main.rs
43
src/main.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue