Implement pagination (experimental)
This commit is contained in:
parent
b894052922
commit
328077d9a1
|
@ -16,6 +16,16 @@ impl Search {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SearchNext {
|
||||||
|
limit: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchNext {
|
||||||
|
pub fn new(limit: usize) -> Self {
|
||||||
|
Self { limit }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Command for Grab {
|
impl Command for Grab {
|
||||||
async fn execute(&mut self, msg: Context<'_>) -> anyhow::Result<String> {
|
async fn execute(&mut self, msg: Context<'_>) -> anyhow::Result<String> {
|
||||||
|
@ -73,14 +83,39 @@ impl Command for Search {
|
||||||
} else {
|
} else {
|
||||||
return Ok("Invalid usage.".into());
|
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() {
|
if results.is_empty() {
|
||||||
return Ok("No results.".into());
|
return Ok("No results.".into());
|
||||||
}
|
}
|
||||||
let mut buf = format!("{}/{} results:\r\n", results.len(), self.limit);
|
let mut buf = String::new();
|
||||||
for q in results {
|
for q in &results {
|
||||||
write!(buf, "\"{}\" ~{}\r\n", q.quote, q.author)?;
|
write!(buf, "\"{}\" ~{}\r\n", q.quote, q.author)?;
|
||||||
}
|
}
|
||||||
|
if results.len() == self.limit {
|
||||||
|
buf.push_str("Use 'qnext' for more results.");
|
||||||
|
}
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Command for SearchNext {
|
||||||
|
async fn execute(&mut self, msg: Context<'_>) -> anyhow::Result<String> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use rusqlite::{params, OptionalExtension, Params};
|
use rusqlite::{params, OptionalExtension, Params};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||||
oneshot,
|
oneshot,
|
||||||
};
|
};
|
||||||
|
use tokio::time::Instant;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Task {
|
enum Task {
|
||||||
|
@ -12,7 +14,8 @@ enum Task {
|
||||||
oneshot::Sender<rusqlite::Result<Option<Quote>>>,
|
oneshot::Sender<rusqlite::Result<Option<Quote>>>,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
),
|
),
|
||||||
SearchQuotes(oneshot::Sender<rusqlite::Result<Vec<Quote>>>, String, usize),
|
StartSearch(oneshot::Sender<rusqlite::Result<Vec<Quote>>>, String, String, usize),
|
||||||
|
NextSearch(oneshot::Sender<rusqlite::Result<Option<Vec<Quote>>>>, String, usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DbExecutor {
|
pub struct DbExecutor {
|
||||||
|
@ -39,7 +42,10 @@ impl DbExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(mut self) {
|
pub fn run(mut self) {
|
||||||
|
let mut searches: HashMap<String, (String, i64)> = HashMap::new();
|
||||||
while let Some(task) = self.rx.blocking_recv() {
|
while let Some(task) = self.rx.blocking_recv() {
|
||||||
|
let before = Instant::now();
|
||||||
|
tracing::debug!("got task {:?}", task);
|
||||||
match task {
|
match task {
|
||||||
Task::AddQuote(tx, quote) => {
|
Task::AddQuote(tx, quote) => {
|
||||||
let result = self
|
let result = self
|
||||||
|
@ -59,26 +65,52 @@ impl DbExecutor {
|
||||||
}.optional();
|
}.optional();
|
||||||
tx.send(result).unwrap();
|
tx.send(result).unwrap();
|
||||||
}
|
}
|
||||||
Task::SearchQuotes(tx, query, limit) => {
|
Task::StartSearch(tx, user, 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();
|
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<P: Params>(&self, sql: &str, params: P) -> rusqlite::Result<Vec<Quote>> {
|
fn start_search(&self, searches: &mut HashMap<String, (String, i64)>, user: String, query: String, limit: usize) -> rusqlite::Result<Vec<Quote>> {
|
||||||
self.db.prepare(sql).and_then(|mut v| {
|
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<String, (String, i64)>, user: &str, limit: usize) -> rusqlite::Result<Option<Vec<Quote>>> {
|
||||||
|
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<P: Params>(&self, sql: &str, params: P) -> rusqlite::Result<(Vec<Quote>, i64)> {
|
||||||
|
let mut lastoid = -1i64;
|
||||||
|
let quotes = self.db.prepare(sql).and_then(|mut v| {
|
||||||
v.query(params).and_then(|mut v| {
|
v.query(params).and_then(|mut v| {
|
||||||
let mut quotes: Vec<Quote> = Vec::new();
|
let mut quotes: Vec<Quote> = Vec::new();
|
||||||
while let Some(row) = v.next()? {
|
while let Some(row) = v.next()? {
|
||||||
|
lastoid = row.get(0)?;
|
||||||
quotes.push(Quote {
|
quotes.push(Quote {
|
||||||
quote: row.get(0)?,
|
quote: row.get(1)?,
|
||||||
author: row.get(1)?,
|
author: row.get(2)?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(quotes)
|
Ok(quotes)
|
||||||
})
|
})
|
||||||
})
|
})?;
|
||||||
|
Ok((quotes, lastoid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,9 +159,17 @@ impl ExecutorConnection {
|
||||||
);
|
);
|
||||||
executor_wrapper!(
|
executor_wrapper!(
|
||||||
search_quotes,
|
search_quotes,
|
||||||
Task::SearchQuotes,
|
Task::StartSearch,
|
||||||
rusqlite::Result<Vec<Quote>>,
|
rusqlite::Result<Vec<Quote>>,
|
||||||
|
user: String,
|
||||||
query: String,
|
query: String,
|
||||||
limit: usize
|
limit: usize
|
||||||
);
|
);
|
||||||
|
executor_wrapper!(
|
||||||
|
advance_search,
|
||||||
|
Task::NextSearch,
|
||||||
|
rusqlite::Result<Option<Vec<Quote>>>,
|
||||||
|
user: String,
|
||||||
|
limit: usize
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use crate::bot::Bot;
|
||||||
use crate::commands::eval::Eval;
|
use crate::commands::eval::Eval;
|
||||||
use crate::commands::help::Help;
|
use crate::commands::help::Help;
|
||||||
use crate::commands::leek::{Leet, Mock, Owo};
|
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::sed::Sed;
|
||||||
use crate::commands::spotify::Spotify;
|
use crate::commands::spotify::Spotify;
|
||||||
use crate::commands::title::Title;
|
use crate::commands::title::Title;
|
||||||
|
@ -110,7 +110,9 @@ async fn main() -> anyhow::Result<()> {
|
||||||
bot.add_command("ev".into(), Eval::default());
|
bot.add_command("ev".into(), Eval::default());
|
||||||
bot.add_command("grab".into(), Grab);
|
bot.add_command("grab".into(), Grab);
|
||||||
bot.add_command("quot".into(), Quot);
|
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(
|
bot.add_trigger(
|
||||||
Regex::new(r"^(?:(?<u>\S+):\s+)?s/(?<r>[^/]*)/(?<w>[^/]*)(?:/(?<f>[a-z]*))?\s*")?,
|
Regex::new(r"^(?:(?<u>\S+):\s+)?s/(?<r>[^/]*)/(?<w>[^/]*)(?:/(?<f>[a-z]*))?\s*")?,
|
||||||
Sed,
|
Sed,
|
||||||
|
|
Loading…
Reference in a new issue