diff --git a/src/commands/quotes.rs b/src/commands/quotes.rs index f85c7ac..e3788d3 100644 --- a/src/commands/quotes.rs +++ b/src/commands/quotes.rs @@ -16,6 +16,16 @@ impl Search { } } +pub struct SearchNext { + limit: usize +} + +impl SearchNext { + pub fn new(limit: usize) -> Self { + Self { limit } + } +} + #[async_trait] impl Command for Grab { async fn execute(&mut self, msg: Context<'_>) -> anyhow::Result { @@ -73,14 +83,39 @@ impl Command for Search { } else { return Ok("Invalid usage.".into()); }; - let results = msg.db.search_quotes(query.into(), self.limit).await?; + let results = msg.db.search_quotes(msg.author.into(), query.into(), self.limit).await?; if results.is_empty() { return Ok("No results.".into()); } - let mut buf = format!("{}/{} results:\r\n", results.len(), self.limit); - for q in results { + let mut buf = String::new(); + for q in &results { write!(buf, "\"{}\" ~{}\r\n", q.quote, q.author)?; } + if results.len() == self.limit { + buf.push_str("Use 'qnext' for more results."); + } Ok(buf) } -} \ No newline at end of file +} + +#[async_trait] +impl Command for SearchNext { + async fn execute(&mut self, msg: Context<'_>) -> anyhow::Result { + let results = if let Some(o) = msg.db.advance_search(msg.author.into(), self.limit).await? { + o + } else { + return Ok("You need to initiate a search first using 'qsearch'.".into()) + }; + if results.is_empty() { + return Ok("No results.".into()); + } + let mut buf = String::new(); + for q in &results { + write!(buf, "\"{}\" ~{}\r\n", q.quote, q.author)?; + } + if results.len() == self.limit { + buf.push_str("Use 'qnext' again for more results."); + } + Ok(buf) + } +} diff --git a/src/database.rs b/src/database.rs index 86b52bc..d79631d 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,9 +1,11 @@ +use std::collections::HashMap; use rusqlite::{params, OptionalExtension, Params}; use serde::Serialize; use tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, oneshot, }; +use tokio::time::Instant; #[derive(Debug)] enum Task { @@ -12,7 +14,8 @@ enum Task { oneshot::Sender>>, Option, ), - SearchQuotes(oneshot::Sender>>, String, usize), + StartSearch(oneshot::Sender>>, String, String, usize), + NextSearch(oneshot::Sender>>>, String, usize) } pub struct DbExecutor { @@ -39,7 +42,10 @@ impl DbExecutor { } pub fn run(mut self) { + let mut searches: HashMap = HashMap::new(); while let Some(task) = self.rx.blocking_recv() { + let before = Instant::now(); + tracing::debug!("got task {:?}", task); match task { Task::AddQuote(tx, quote) => { let result = self @@ -59,26 +65,52 @@ impl DbExecutor { }.optional(); tx.send(result).unwrap(); } - Task::SearchQuotes(tx, query, limit) => { - tx.send(self.yield_quotes("select quote,username from quotes where quote like '%'||?1||'%' order by quote asc limit ?", params![query, limit])).unwrap(); + Task::StartSearch(tx, user, query, limit) => { + tx.send(self.start_search(&mut searches, user, query, limit)).unwrap(); + } + Task::NextSearch(tx, user, limit) => { + tx.send(self.next_search(&mut searches, &user, limit)).unwrap(); } } + tracing::debug!("task took {}ms", Instant::now().duration_since(before).as_secs_f64()/1000.0) } } - fn yield_quotes(&self, sql: &str, params: P) -> rusqlite::Result> { - self.db.prepare(sql).and_then(|mut v| { + fn start_search(&self, searches: &mut HashMap, user: String, query: String, limit: usize) -> rusqlite::Result> { + let (quotes, oid) = self.yield_quotes_oid("select oid,quote,username from quotes where quote like '%'||?1||'%' order by oid asc limit ?", params![query, limit])?; + searches.insert(user, (query, oid)); + Ok(quotes) + } + + fn next_search(&self, searches: &mut HashMap, user: &str, limit: usize) -> rusqlite::Result>> { + let (query, old_oid) = if let Some(o) = searches.get_mut(user) { + o + } else { + return Ok(None) + }; + let (quotes, new_oid) = self.yield_quotes_oid("select oid,quote,username from quotes where oid > ? and quote like '%'||?||'%' order by oid asc limit ?", params![*old_oid, &*query, limit])?; + if new_oid != -1 { + *old_oid = new_oid; + } + Ok(Some(quotes)) + } + + fn yield_quotes_oid(&self, sql: &str, params: P) -> rusqlite::Result<(Vec, i64)> { + let mut lastoid = -1i64; + let quotes = self.db.prepare(sql).and_then(|mut v| { v.query(params).and_then(|mut v| { let mut quotes: Vec = Vec::new(); while let Some(row) = v.next()? { + lastoid = row.get(0)?; quotes.push(Quote { - quote: row.get(0)?, - author: row.get(1)?, + quote: row.get(1)?, + author: row.get(2)?, }); } Ok(quotes) }) - }) + })?; + Ok((quotes, lastoid)) } } @@ -127,9 +159,17 @@ impl ExecutorConnection { ); executor_wrapper!( search_quotes, - Task::SearchQuotes, + Task::StartSearch, rusqlite::Result>, + user: String, query: String, limit: usize ); + executor_wrapper!( + advance_search, + Task::NextSearch, + rusqlite::Result>>, + user: String, + limit: usize + ); } diff --git a/src/main.rs b/src/main.rs index d7cb283..e474d88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use crate::bot::Bot; use crate::commands::eval::Eval; use crate::commands::help::Help; use crate::commands::leek::{Leet, Mock, Owo}; -use crate::commands::quotes::{Grab, Quot, Search}; +use crate::commands::quotes::{Grab, Quot, Search, SearchNext}; use crate::commands::sed::Sed; use crate::commands::spotify::Spotify; use crate::commands::title::Title; @@ -110,7 +110,9 @@ async fn main() -> anyhow::Result<()> { bot.add_command("ev".into(), Eval::default()); bot.add_command("grab".into(), Grab); bot.add_command("quot".into(), Quot); - bot.add_command("qsearch".into(), Search::new(cfg.bot.search_limit.unwrap_or(3))); + let search_limit = cfg.bot.search_limit.unwrap_or(3); + bot.add_command("qsearch".into(), Search::new(search_limit)); + bot.add_command("qnext".into(), SearchNext::new(search_limit)); bot.add_trigger( Regex::new(r"^(?:(?\S+):\s+)?s/(?[^/]*)/(?[^/]*)(?:/(?[a-z]*))?\s*")?, Sed,