what2watch/src/watches/handlers.rs
2024-01-01 17:27:17 -08:00

318 lines
9.6 KiB
Rust

use axum::{
extract::{Form, Path, Query, State},
http::StatusCode,
response::{IntoResponse, Redirect, Response},
};
use http::HeaderValue;
use julid::Julid;
use serde::Deserialize;
use sqlx::{query, query_as, query_scalar, SqlitePool};
use super::templates::{AddNewWatchPage, AddWatchButton, GetWatchPage, SearchWatchesPage};
use crate::{
util::{empty_string_as_none, year_to_epoch},
AuthSession, MyWatchesPage, ShowKind, Watch, WatchQuest,
};
//-************************************************************************
// Constants
//-************************************************************************
const GET_SAVED_WATCHES_QUERY: &str = "select * from watches inner join watch_quests quests on quests.user = $1 and quests.watch = watches.id";
const GET_QUEST_WITH_PUBLICITY_QUERY: &str =
"select * from watch_quests where user = ? and watch = ? and public = ?";
const GET_QUEST_WITH_WATCHED_QUERY: &str =
"select * from watch_quests where user = ? and watch = ? and watched = ?";
const GET_WATCH_QUERY: &str = "select * from watches where id = $1";
const ADD_WATCH_QUERY: &str = "insert into watches (title, kind, release_date, metadata_url, added_by, length) values ($1, $2, $3, $4, $5, $6) returning id";
const ADD_WATCH_QUEST_QUERY: &str =
"insert into watch_quests (user, watch, public, watched) values ($1, $2, $3, $4)";
const EMPTY_SEARCH_QUERY_STRUCT: SearchQuery = SearchQuery {
title: None,
kind: None,
year: None,
search: None,
};
//-************************************************************************
// Error types for Watch creation
//-************************************************************************
#[Error]
pub struct AddError(#[from] AddErrorKind);
#[Error]
#[non_exhaustive]
pub enum AddErrorKind {
UnknownDBError,
NotSignedIn,
}
impl IntoResponse for AddError {
fn into_response(self) -> Response {
match &self.0 {
AddErrorKind::UnknownDBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
AddErrorKind::NotSignedIn => (
StatusCode::OK,
"Ope, you need to sign in first!".to_string(),
)
.into_response(),
}
}
}
//-************************************************************************
// Types for receiving arguments from forms
//-************************************************************************
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
pub struct SearchQuery {
#[serde(default, deserialize_with = "empty_string_as_none")]
pub search: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub title: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub kind: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub year: Option<i64>,
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
pub struct StatusQuery {
pub public: bool,
}
// kinda the main form?
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
pub struct PostAddNewWatch {
pub title: String,
#[serde(default)]
pub private: bool,
pub kind: ShowKind,
#[serde(default, deserialize_with = "empty_string_as_none")]
pub year: Option<String>, // need a date-picker or something
#[serde(default, deserialize_with = "empty_string_as_none")]
pub metadata_url: Option<String>,
#[serde(default)]
pub watched_already: bool,
}
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
pub struct PostAddExistingWatch {
pub watch: String,
pub public: bool,
pub watched_already: bool,
}
//-************************************************************************
// handlers
//-************************************************************************
pub async fn get_add_new_watch(auth: AuthSession) -> impl IntoResponse {
AddNewWatchPage { user: auth.user }
}
/// Add a Watch to your watchlist (side effects system-add)
pub async fn post_add_new_watch(
auth: AuthSession,
State(pool): State<SqlitePool>,
Form(form): Form<PostAddNewWatch>,
) -> Result<impl IntoResponse, AddError> {
if let Some(user) = auth.user {
{
let release_date = year_to_epoch(form.year.as_deref());
let watch = Watch {
title: form.title,
kind: form.kind,
metadata_url: form.metadata_url,
release_date,
added_by: user.id,
..Default::default()
};
let watch_id = add_new_watch_impl(&pool, &watch).await?;
let quest = WatchQuest {
user: user.id,
public: !form.private,
watched: form.watched_already,
watch: watch_id,
};
add_watch_quest_impl(&pool, &quest)
.await
.map_err(|_| AddErrorKind::UnknownDBError)?;
let location = format!("/watch/{watch_id}");
Ok(Redirect::to(&location))
}
} else {
Err(AddErrorKind::NotSignedIn.into())
}
}
async fn add_new_watch_impl(db_pool: &SqlitePool, watch: &Watch) -> Result<Julid, AddError> {
let watch_id: Julid = query_scalar(ADD_WATCH_QUERY)
.bind(&watch.title)
.bind(watch.kind)
.bind(watch.release_date)
.bind(&watch.metadata_url)
.bind(watch.added_by)
.bind(watch.length)
.fetch_one(db_pool)
.await
.map_err(|err| {
tracing::error!("Got error: {err}");
AddErrorKind::UnknownDBError
})?;
Ok(watch_id)
}
/// Add a Watch to your watchlist by selecting it with a checkbox
pub async fn post_add_watch_quest(
auth: AuthSession,
State(pool): State<SqlitePool>,
Form(form): Form<PostAddExistingWatch>,
) -> Result<impl IntoResponse, AddError> {
if let Some(user) = auth.user {
let quest = WatchQuest {
user: user.id,
watch: Julid::from_string(&form.watch).unwrap(),
public: form.public,
watched: form.watched_already,
};
add_watch_quest_impl(&pool, &quest)
.await
.map_err(|_| AddErrorKind::UnknownDBError)?;
let resp = "<style=\"background-color:green\">&#10003;</style>";
Ok(resp.into_response())
} else {
let resp = Redirect::to("/login");
let mut resp = resp.into_response();
resp.headers_mut()
.insert("HX-Redirect", HeaderValue::from_str("/login").unwrap());
Ok(resp)
}
}
pub async fn add_watch_quest_impl(pool: &SqlitePool, quest: &WatchQuest) -> Result<(), ()> {
query(ADD_WATCH_QUEST_QUERY)
.bind(quest.user)
.bind(quest.watch)
.bind(quest.public)
.bind(quest.watched)
.execute(pool)
.await
.map_err(|err| {
tracing::error!("Got error: {err}");
})?;
Ok(())
}
/// A single Watch
pub async fn get_watch(
auth: AuthSession,
watch: Option<Path<String>>,
State(pool): State<SqlitePool>,
) -> impl IntoResponse {
let id = if let Some(Path(id)) = watch {
id
} else {
"".to_string()
};
let id = id.trim();
let id = Julid::from_string(id).unwrap_or_default();
let q = query_as(GET_WATCH_QUERY).bind(id);
let watch: Option<Watch> = q.fetch_one(&pool).await.ok();
GetWatchPage {
watch,
user: auth.user,
}
}
/// everything the user has saved
pub async fn get_watches(auth: AuthSession, State(pool): State<SqlitePool>) -> impl IntoResponse {
let user = auth.user;
let watches: Vec<Watch> = if (user).is_some() {
query_as(GET_SAVED_WATCHES_QUERY)
.bind(user.as_ref().unwrap().id)
.fetch_all(&pool)
.await
.unwrap_or_default()
} else {
vec![]
};
MyWatchesPage { watches, user }
}
pub async fn get_search_watch(
auth: AuthSession,
State(pool): State<SqlitePool>,
search: Query<SearchQuery>,
) -> impl IntoResponse {
let user = auth.user;
let (search_string, qstring) = if search.0 != EMPTY_SEARCH_QUERY_STRUCT {
let s = search.0;
let q = if let Some(title) = &s.title {
title
} else if let Some(search) = &s.search {
search
} else {
""
};
(format!("{s:?}"), format!("%{}%", q.trim()))
} else {
("".to_string(), "%".to_string())
};
// until tantivy search
let watches: Vec<Watch> = query_as("select * from watches where title like ?")
.bind(&qstring)
.fetch_all(&pool)
.await
.unwrap();
SearchWatchesPage {
watches,
user,
search: search_string,
}
}
pub async fn get_watch_status(
auth: AuthSession,
State(pool): State<SqlitePool>,
query: Query<StatusQuery>,
Path(watch): Path<String>,
) -> Result<impl IntoResponse, ()> {
if let Some(user) = auth.user {
let watch = Julid::from_string(&watch).unwrap();
let public = query.public;
let quest: Option<WatchQuest> = query_as(GET_QUEST_WITH_PUBLICITY_QUERY)
.bind(user.id)
.bind(watch)
.bind(public)
.fetch_optional(&pool)
.await
.map_err(|e| {
tracing::error!("Got error from checking watch status: {e:?}");
})?;
match quest {
Some(_) => Ok("&#10003;".into_response()),
None => Ok(AddWatchButton {
watch,
public,
watched_already: false,
}
.into_response()),
}
} else {
Ok("<a href='/login'>Login to add</a>".into_response())
}
}