Port quotquot
This commit is contained in:
parent
05d4569c5e
commit
c3b12e9e72
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@ uberbot_*.toml
|
||||||
uberbot.toml
|
uberbot.toml
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
.idea
|
.idea
|
||||||
|
*.db3
|
||||||
|
|
|
@ -22,5 +22,6 @@ arrayvec = "0.7"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
meval = "0.2"
|
meval = "0.2"
|
||||||
async-circe = { git = "https://git.karx.xyz/circe/async-circe" }
|
async-circe = { git = "https://git.karx.xyz/circe/async-circe" }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4"
|
||||||
sedregex = "0.2.5"
|
sedregex = "0.2"
|
||||||
|
rusqlite = { version = "0.26", features = ["bundled"] }
|
||||||
|
|
|
@ -10,7 +10,6 @@ pub async fn get_waifu_pic(category: &str) -> anyhow::Result<Option<String>> {
|
||||||
.text()
|
.text()
|
||||||
.await?;
|
.await?;
|
||||||
let api_resp = api_resp.trim();
|
let api_resp = api_resp.trim();
|
||||||
tracing::debug!("API response: {}", api_resp);
|
|
||||||
let value: Value = serde_json::from_str(&api_resp)?;
|
let value: Value = serde_json::from_str(&api_resp)?;
|
||||||
let url = value["url"].as_str().map(|v| v.to_string());
|
let url = value["url"].as_str().map(|v| v.to_string());
|
||||||
Ok(url)
|
Ok(url)
|
||||||
|
|
75
src/database.rs
Normal file
75
src/database.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use rusqlite::{OptionalExtension, params};
|
||||||
|
use tokio::sync::{mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, oneshot};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Task {
|
||||||
|
AddQuote(oneshot::Sender<bool>, String, String),
|
||||||
|
GetQuote(oneshot::Sender<Option<(String, String)>>, Option<String>),
|
||||||
|
// implement search WITH PAGINATION
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DbExecutor {
|
||||||
|
rx: UnboundedReceiver<Task>,
|
||||||
|
db: rusqlite::Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbExecutor {
|
||||||
|
pub fn create(dbpath: &str) -> rusqlite::Result<(Self, ExecutorConnection)> {
|
||||||
|
let (tx, rx) = unbounded_channel();
|
||||||
|
let db = rusqlite::Connection::open(dbpath)?;
|
||||||
|
db.execute("create table if not exists quotes(id integer primary key,\
|
||||||
|
username text not null, quote text not null)", [])?;
|
||||||
|
tracing::debug!("Database connected ({})", dbpath);
|
||||||
|
Ok((Self { rx, db }, ExecutorConnection { tx }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self) {
|
||||||
|
while let Some(task) = self.rx.blocking_recv() {
|
||||||
|
match task {
|
||||||
|
Task::AddQuote(tx, quote, author) => {
|
||||||
|
if let Err(e) = self.db.execute(
|
||||||
|
"insert into quotes(quote,username) values(?,?)", params![quote,author]) {
|
||||||
|
tracing::error!("A database error has occurred: {}", e);
|
||||||
|
tx.send(false).unwrap();
|
||||||
|
} else {
|
||||||
|
tx.send(true).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::GetQuote(tx, author) => {
|
||||||
|
let quote = if let Some(ref author) = author {
|
||||||
|
self.db.query_row("select quote,username from quotes where username=? order by random() limit 1", params![author], |v| Ok((v.get(0)?, v.get(1)?)))
|
||||||
|
} else {
|
||||||
|
self.db.query_row("select quote,username from quotes order by random() limit 1", params![], |v| Ok((v.get(0)?, v.get(1)?)))
|
||||||
|
}.optional().unwrap_or_else(|e| {
|
||||||
|
tracing::error!("A database error has occurred: {}", e);
|
||||||
|
None
|
||||||
|
});
|
||||||
|
tx.send(quote).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExecutorConnection {
|
||||||
|
tx: UnboundedSender<Task>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for ExecutorConnection {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self { tx: self.tx.clone() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExecutorConnection {
|
||||||
|
pub async fn add_quote(&self, quote: String, author: String) -> bool {
|
||||||
|
let (otx, orx) = oneshot::channel();
|
||||||
|
self.tx.send(Task::AddQuote(otx, quote, author)).unwrap();
|
||||||
|
orx.await.unwrap()
|
||||||
|
}
|
||||||
|
pub async fn get_quote(&self, author: Option<String>) -> Option<(String, String)> {
|
||||||
|
let (otx, orx) = oneshot::channel();
|
||||||
|
self.tx.send(Task::GetQuote(otx, author)).unwrap();
|
||||||
|
orx.await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
57
src/main.rs
57
src/main.rs
|
@ -1,4 +1,5 @@
|
||||||
mod bots;
|
mod bots;
|
||||||
|
mod database;
|
||||||
|
|
||||||
use arrayvec::ArrayString;
|
use arrayvec::ArrayString;
|
||||||
use async_circe::{commands::Command, Client, Config};
|
use async_circe::{commands::Command, Client, Config};
|
||||||
|
@ -12,6 +13,8 @@ use std::{collections::HashMap, env};
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
use std::thread;
|
||||||
|
use crate::database::{DbExecutor, ExecutorConnection};
|
||||||
|
|
||||||
// this will be displayed when the help command is used
|
// this will be displayed when the help command is used
|
||||||
const HELP: &[&str] = &[
|
const HELP: &[&str] = &[
|
||||||
|
@ -48,6 +51,7 @@ struct AppState {
|
||||||
last_msgs: HashMap<String, String>,
|
last_msgs: HashMap<String, String>,
|
||||||
last_eval: HashMap<String, f64>,
|
last_eval: HashMap<String, f64>,
|
||||||
titlebot: Titlebot,
|
titlebot: Titlebot,
|
||||||
|
db: ExecutorConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -61,6 +65,7 @@ struct ClientConf {
|
||||||
spotify_client_id: String,
|
spotify_client_id: String,
|
||||||
spotify_client_secret: String,
|
spotify_client_secret: String,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
|
db_path: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
@ -75,6 +80,12 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let client_config: ClientConf = toml::from_str(&client_conf)?;
|
let client_config: ClientConf = toml::from_str(&client_conf)?;
|
||||||
|
|
||||||
|
let (db_exec, db_conn) = DbExecutor::create(client_config.db_path.as_deref().unwrap_or("uberbot.db3"))?;
|
||||||
|
thread::spawn(move || {
|
||||||
|
db_exec.run();
|
||||||
|
tracing::info!("Database executor has been shut down");
|
||||||
|
});
|
||||||
|
|
||||||
let spotify_creds = Credentials::new(
|
let spotify_creds = Credentials::new(
|
||||||
&client_config.spotify_client_id,
|
&client_config.spotify_client_id,
|
||||||
&client_config.spotify_client_secret,
|
&client_config.spotify_client_secret,
|
||||||
|
@ -97,6 +108,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
last_msgs: HashMap::new(),
|
last_msgs: HashMap::new(),
|
||||||
last_eval: HashMap::new(),
|
last_eval: HashMap::new(),
|
||||||
titlebot: Titlebot::create(spotify_creds).await?,
|
titlebot: Titlebot::create(spotify_creds).await?,
|
||||||
|
db: db_conn
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = executor(state).await {
|
if let Err(e) = executor(state).await {
|
||||||
|
@ -133,12 +145,14 @@ async fn message_loop(state: &mut AppState) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum LeekCommand {
|
enum LeekCommand {
|
||||||
Owo, Leet, Mock
|
Owo, Leet, Mock
|
||||||
}
|
}
|
||||||
async fn execute_leek(state: &mut AppState, cmd: LeekCommand, channel: &str, nick: &str) -> anyhow::Result<()> {
|
async fn execute_leek(state: &mut AppState, cmd: LeekCommand, channel: &str, nick: &str) -> anyhow::Result<()> {
|
||||||
match state.last_msgs.get(nick) {
|
match state.last_msgs.get(nick) {
|
||||||
Some(msg) => {
|
Some(msg) => {
|
||||||
|
tracing::debug!("Executing {:?} on {:?}", cmd, msg);
|
||||||
let output = match cmd {
|
let output = match cmd {
|
||||||
LeekCommand::Owo => leek::owoify(msg)?,
|
LeekCommand::Owo => leek::owoify(msg)?,
|
||||||
LeekCommand::Leet => leek::leetify(msg)?,
|
LeekCommand::Leet => leek::leetify(msg)?,
|
||||||
|
@ -153,6 +167,14 @@ async fn execute_leek(state: &mut AppState, cmd: LeekCommand, channel: &str, nic
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn separate_to_space(str: &str, prefix_len: usize) -> (&str, Option<&str>) {
|
||||||
|
if let Some(o) = str.find(' ') {
|
||||||
|
(&str[prefix_len..o], Some(&str[o + 1..]))
|
||||||
|
} else {
|
||||||
|
(&str[prefix_len..], None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_privmsg(
|
async fn handle_privmsg(
|
||||||
state: &mut AppState,
|
state: &mut AppState,
|
||||||
nick: String,
|
nick: String,
|
||||||
|
@ -177,12 +199,7 @@ async fn handle_privmsg(
|
||||||
state.last_msgs.insert(nick, message);
|
state.last_msgs.insert(nick, message);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let space_index = message.find(' ');
|
let (command, remainder) = separate_to_space(&message, state.prefix.len());
|
||||||
let (command, remainder) = if let Some(o) = space_index {
|
|
||||||
(&message[state.prefix.len()..o], Some(&message[o + 1..]))
|
|
||||||
} else {
|
|
||||||
(&message[state.prefix.len()..], None)
|
|
||||||
};
|
|
||||||
tracing::debug!("Command received ({:?}; {:?})", command, remainder);
|
tracing::debug!("Command received ({:?}; {:?})", command, remainder);
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
|
@ -213,6 +230,34 @@ async fn handle_privmsg(
|
||||||
let result = misc::mathbot(nick, remainder, &mut state.last_eval)?;
|
let result = misc::mathbot(nick, remainder, &mut state.last_eval)?;
|
||||||
state.client.privmsg(&channel, &result).await?;
|
state.client.privmsg(&channel, &result).await?;
|
||||||
}
|
}
|
||||||
|
"grab" => {
|
||||||
|
if let Some(target) = remainder {
|
||||||
|
if target == nick {
|
||||||
|
state.client.privmsg(&channel, "You can't grab yourself").await?;
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
if let Some(prev_msg) = state.last_msgs.get(target) {
|
||||||
|
if state.db.add_quote(prev_msg.clone(), target.into()).await {
|
||||||
|
state.client.privmsg(&channel, "Quote added").await?;
|
||||||
|
} else {
|
||||||
|
state.client.privmsg(&channel, "A database error has occurred").await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.client.privmsg(&channel, "No previous messages to grab").await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.client.privmsg(&channel, "No nickname to grab").await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"quot" => {
|
||||||
|
if let Some(quote) = state.db.get_quote(remainder.map(|v| v.to_string())).await {
|
||||||
|
let mut resp = ArrayString::<512>::new();
|
||||||
|
write!(resp, "\"{}\" ~{}", quote.0, quote.1)?;
|
||||||
|
state.client.privmsg(&channel, &resp).await?;
|
||||||
|
} else {
|
||||||
|
state.client.privmsg(&channel, "No quotes found").await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
state.client.privmsg(&channel, "Unknown command").await?;
|
state.client.privmsg(&channel, "Unknown command").await?;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue