Fix search in the weeb service

This commit is contained in:
lemon-sh 2022-01-28 00:44:50 +01:00
parent 49d69cef5c
commit 336ed231f7
5 changed files with 118 additions and 41 deletions

View file

@ -1,4 +1,4 @@
use arrayvec::{ArrayString}; use arrayvec::ArrayString;
use rand::Rng; use rand::Rng;
use std::{ use std::{
error::Error, error::Error,

View file

@ -1,15 +1,16 @@
use rusqlite::{params, OptionalExtension}; use rusqlite::{params, OptionalExtension};
use serde::Serialize;
use tokio::sync::{ use tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
oneshot, oneshot,
}; };
use serde::Serialize;
#[derive(Debug)] #[derive(Debug)]
enum Task { enum Task {
AddQuote(oneshot::Sender<bool>, Quote), AddQuote(oneshot::Sender<bool>, Quote),
GetQuote(oneshot::Sender<Option<Quote>>, Option<String>), GetQuote(oneshot::Sender<Option<Quote>>, Option<String>),
// implement search WITH PAGINATION Search(oneshot::Sender<Option<Vec<Quote>>>, String),
Random20(oneshot::Sender<Option<Vec<Quote>>>)
} }
pub struct DbExecutor { pub struct DbExecutor {
@ -20,7 +21,7 @@ pub struct DbExecutor {
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct Quote { pub struct Quote {
pub author: String, pub author: String,
pub quote: String pub quote: String,
} }
impl DbExecutor { impl DbExecutor {
@ -61,6 +62,33 @@ impl DbExecutor {
}); });
tx.send(quote).unwrap(); tx.send(quote).unwrap();
} }
Task::Search(tx, query) => {
tx.send(match self.db
.prepare("select quote,username from quotes where quote like '%'||?1||'%' order by quote asc limit 50")
.and_then(|mut v| v.query(params![query])
.and_then(|mut v| {
let mut quotes: Vec<Quote> = Vec::with_capacity(50);
while let Some(row) = v.next()? {
quotes.push(Quote {
quote: row.get(0)?,
author: row.get(1)?,
});
}
Ok(quotes)
}))
{
Ok(o) => {
Some(o)
}
Err(e) => {
tracing::error!("A database error has occurred: {}", e);
None
}
}).unwrap();
}
Task::Random20(tx) => {
tx.send(None).unwrap();
}
} }
} }
} }
@ -78,15 +106,24 @@ impl Clone for ExecutorConnection {
} }
} }
impl ExecutorConnection { macro_rules! executor_wrapper {
pub async fn add_quote(&self, quote: Quote) -> bool { ($name:ident, $task:expr, $ret:ty, $($arg:ident: $ty:ty),*) => {
let (otx, orx) = oneshot::channel(); pub async fn $name(&self, $($arg: $ty),*) -> $ret {
self.tx.send(Task::AddQuote(otx, quote)).unwrap(); let (otx, orx) = oneshot::channel();
orx.await.unwrap() self.tx.send($task(otx, $($arg),*)).unwrap();
} orx.await.unwrap()
pub async fn get_quote(&self, author: Option<String>) -> Option<Quote> {
let (otx, orx) = oneshot::channel();
self.tx.send(Task::GetQuote(otx, author)).unwrap();
orx.await.unwrap()
} }
}
}
impl ExecutorConnection {
// WARNING: these methods are NOT cancel-safe
executor_wrapper!(add_quote, Task::AddQuote, bool, quote: Quote);
executor_wrapper!(
get_quote,
Task::GetQuote,
Option<Quote>,
author: Option<String>
);
executor_wrapper!(search, Task::Search, Option<Vec<Quote>>, query: String);
} }

View file

@ -16,7 +16,7 @@ use irc::proto::{ChannelExt, Command, Prefix};
use rspotify::Credentials; use rspotify::Credentials;
use serde::Deserialize; use serde::Deserialize;
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::bots::{leek, misc, sed, title}; use crate::bots::{leek, misc, sed, title};
@ -142,7 +142,7 @@ async fn main() -> anyhow::Result<()> {
client.clone(), client.clone(),
client_config.git_channel, client_config.git_channel,
http_listen, http_listen,
ctx.subscribe() ctx.subscribe(),
)); ));
let state = AppState { let state = AppState {
@ -258,7 +258,8 @@ async fn handle_privmsg(
"waifu" => { "waifu" => {
let category = remainder.unwrap_or("waifu"); let category = remainder.unwrap_or("waifu");
let url = misc::get_waifu_pic(category).await?; let url = misc::get_waifu_pic(category).await?;
let response = url.as_deref() let response = url
.as_deref()
.unwrap_or("Invalid category. Valid categories: https://waifu.pics/docs"); .unwrap_or("Invalid category. Valid categories: https://waifu.pics/docs");
state.client.send_privmsg(origin, response)?; state.client.send_privmsg(origin, response)?;
} }
@ -299,7 +300,14 @@ async fn handle_privmsg(
return Ok(()); return Ok(());
} }
if let Some(prev_msg) = state.last_msgs.get(target) { if let Some(prev_msg) = state.last_msgs.get(target) {
if state.db.add_quote(Quote{quote:prev_msg.clone(), author:target.into()}).await { if state
.db
.add_quote(Quote {
quote: prev_msg.clone(),
author: target.into(),
})
.await
{
state.client.send_privmsg(target, "Quote added")?; state.client.send_privmsg(target, "Quote added")?;
} else { } else {
state state

View file

@ -16,13 +16,16 @@
</head> </head>
<body> <body>
<main> <main>
<form method="post"> <form method="get">
<label> <label>
Search query Search query
<input type="text" name="query" placeholder="Search..." required> <input type="text" name="query" placeholder="Search..." required>
</label> </label>
<button type="submit">Search</button> <button type="submit">Search</button>
</form> </form>
{{#if flash}}
<p>{{flash}}</p>
{{/if}}
{{#if quotes}} {{#if quotes}}
<table> <table>
<thead> <thead>

View file

@ -1,20 +1,21 @@
use crate::database::Quote;
use crate::ExecutorConnection; use crate::ExecutorConnection;
use handlebars::Handlebars;
use irc::client::Client; use irc::client::Client;
use lazy_static::lazy_static;
use reqwest::StatusCode; use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value::Null; use serde_json::Value::Null;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use handlebars::Handlebars;
use lazy_static::lazy_static;
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
use warp::{reply, Filter, Reply}; use warp::{reply, Filter, Reply};
use serde::Serialize;
use crate::database::Quote;
lazy_static! { lazy_static! {
static ref HANDLEBARS: Handlebars<'static> = { static ref HANDLEBARS: Handlebars<'static> = {
let mut reg = Handlebars::new(); let mut reg = Handlebars::new();
reg.register_template_string("quotes", include_str!("res/quote_tmpl.hbs")).unwrap(); reg.register_template_string("quotes", include_str!("res/quote_tmpl.hbs"))
.unwrap();
reg reg
}; };
} }
@ -24,12 +25,13 @@ pub async fn run(
wh_irc: Arc<Client>, wh_irc: Arc<Client>,
wh_channel: String, wh_channel: String,
listen: SocketAddr, listen: SocketAddr,
mut cancel: Receiver<()> mut cancel: Receiver<()>,
) { ) {
let quote_get = warp::path("quotes") let quote_get = warp::get()
.and(warp::get()) .and(warp::path("quotes"))
.and(warp::query::<QuotesQuery>())
.and(warp::any().map(move || db.clone())) .and(warp::any().map(move || db.clone()))
.map(handle_get_quote); .then(handle_get_quote);
let webhook_post = warp::path("webhook") let webhook_post = warp::path("webhook")
.and(warp::post()) .and(warp::post())
@ -38,31 +40,58 @@ pub async fn run(
.and(warp::any().map(move || wh_channel.clone())) .and(warp::any().map(move || wh_channel.clone()))
.map(handle_webhook); .map(handle_webhook);
let filter = quote_get.or(webhook_post); let routes = webhook_post.or(quote_get);
warp::serve(filter).bind_with_graceful_shutdown(listen, async move { warp::serve(routes)
let _ = cancel.recv().await; .bind_with_graceful_shutdown(listen, async move {
}).1.await; let _ = cancel.recv().await;
})
.1
.await;
tracing::info!("Web service finished"); tracing::info!("Web service finished");
} }
#[derive(Serialize)] #[derive(Serialize)]
struct QuotesTemplate { struct QuotesTemplate {
quotes: Option<Vec<Quote>> quotes: Option<Vec<Quote>>,
flash: Option<String>,
} }
fn handle_get_quote(_: ExecutorConnection) -> impl Reply { #[derive(Deserialize)]
match HANDLEBARS.render("quotes", &QuotesTemplate{quotes: Some(vec![ struct QuotesQuery {
Quote{quote:"something".into(),author:"by someone".into()}, query: Option<String>,
Quote{quote:"something different".into(),author:"by someone else".into()}, }
Quote{quote:"something even more different".into(),author:"by nobody".into()}
])}) { async fn handle_get_quote(query: QuotesQuery, db: ExecutorConnection) -> impl Reply {
let template = if let Some(query) = query.query {
if let Some(quotes) = db.search(query.clone()).await {
let quotes_count = quotes.len();
QuotesTemplate {
quotes: Some(quotes),
flash: Some(format!("Displaying {}/50 results for query \"{}\"", quotes_count, query)),
}
} else {
QuotesTemplate {
quotes: None,
flash: Some("A database error has occurred".into()),
}
}
} else {
QuotesTemplate {
quotes: None,
flash: None,
}
};
match HANDLEBARS.render("quotes", &template) {
Ok(o) => reply::html(o).into_response(), Ok(o) => reply::html(o).into_response(),
Err(e) => { Err(e) => {
tracing::warn!("Error while rendering template: {}", e); tracing::warn!("Error while rendering template: {}", e);
reply::with_status("Failed to render template", StatusCode::INTERNAL_SERVER_ERROR).into_response() reply::with_status(
"Failed to render template",
StatusCode::INTERNAL_SERVER_ERROR,
)
.into_response()
} }
} }
} }
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]