diff --git a/src/db.rs b/src/db.rs index 6224db0..b7d225b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -42,19 +42,15 @@ impl Database { guilds } - pub fn get_all_user_intros(&self, username: &str, guild_id: u64) -> Result> { + pub fn get_guild_intros(&self, guild_id: u64) -> Result> { let mut query = self.conn.prepare( " SELECT Intro.id, - Intro.name, - UI.channel_name + Intro.name FROM Intro - LEFT JOIN UserIntro UI ON UI.intro_id = Intro.id WHERE - UI.username = :username - AND UI.guild_id = :guild_id - ORDER BY UI.channel_name DESC, UI.intro_id; + Intro.guild_id = :guild_id ", )?; @@ -63,7 +59,6 @@ impl Database { let intros = query .query_map( &[ - (":username", username), // :vomit: (":guild_id", &guild_id.to_string()), ], @@ -71,7 +66,6 @@ impl Database { Ok(Intro { id: row.get(0)?, name: row.get(1)?, - channel_name: row.get(2)?, }) }, )? @@ -81,6 +75,47 @@ impl Database { intros } + pub fn get_all_user_intros(&self, guild_id: u64) -> Result> { + let mut query = self.conn.prepare( + " + SELECT + Intro.id, + Intro.name, + UI.channel_name, + UI.username + FROM Intro + LEFT JOIN UserIntro UI ON UI.intro_id = Intro.id + WHERE + UI.guild_id = :guild_id + ORDER BY UI.username DESC, UI.channel_name DESC, UI.intro_id; + ", + )?; + + // NOTE(pcleavelin): for some reason this needs to be a let-binding or else + // the compiler complains about it being dropped too early (maybe I should update the compiler version) + let intros = query + .query_map( + &[ + // :vomit: + (":guild_id", &guild_id.to_string()), + ], + |row| { + Ok(UserIntro { + intro: Intro { + id: row.get(0)?, + name: row.get(1)?, + }, + channel_name: row.get(2)?, + username: row.get(3)?, + }) + }, + )? + .into_iter() + .collect::>>(); + + intros + } + pub(crate) fn get_user_permissions( &self, username: &str, @@ -109,5 +144,10 @@ pub struct Guild { pub struct Intro { pub id: i32, pub name: String, - pub channel_name: String, +} + +pub struct UserIntro { + pub intro: Intro, + pub channel_name: String, + pub username: String, } diff --git a/src/page.rs b/src/page.rs index ac5943c..c919083 100644 --- a/src/page.rs +++ b/src/page.rs @@ -77,6 +77,7 @@ fn intro_list<'a>( HtmxBuilder::new(Tag::Empty).form(|b| { b.attribute("class", "container") .hx_post(post) + .hx_target("closest #channel-intro-selector") .attribute("hx-encoding", "multipart/form-data") .builder(Tag::FieldSet, |b| { let mut b = b @@ -105,18 +106,24 @@ pub(crate) async fn guild_dashboard( ) -> Result, Redirect> { let db = state.db.lock().await; - let user_intros = db - .get_all_user_intros(&user.name, guild_id) - .map_err(|err| { - error!(?err, user = %user.name, %guild_id, "couldn't get user's intros"); - // TODO: change to actual error - Redirect::to("/login") - })?; + let guild_intros = db.get_guild_intros(guild_id).map_err(|err| { + error!(?err, %guild_id, "couldn't get guild intros"); + // TODO: change to actual error + Redirect::to("/login") + })?; + let all_user_intros = db.get_all_user_intros(guild_id).map_err(|err| { + error!(?err, %guild_id, "couldn't get user intros"); + // TODO: change to actual error + Redirect::to("/login") + })?; let user_permissions = db .get_user_permissions(&user.name, guild_id) .unwrap_or_default(); - let channel_user_intros = user_intros.iter().group_by(|intro| &intro.channel_name); + let grouped_intros = all_user_intros.iter().group_by(|intro| &intro.username); + let user_intros = grouped_intros + .into_iter() + .filter(|(username, _)| username == &&user.name); let can_upload = user_permissions.can(auth::Permission::UploadSounds); let is_moderator = user_permissions.can(auth::Permission::DeleteSounds); @@ -168,74 +175,28 @@ pub(crate) async fn guild_dashboard( .builder(Tag::Article, |b| { let mut b = b.builder_text(Tag::Header, "Guild Intros"); - for (channel_name, intros) in &channel_user_intros { - b = b.builder(Tag::Article, |b| { - b.builder_text(Tag::Header, &channel_name).builder( - Tag::Div, - |b| { - b.builder_text(Tag::Strong, "Your Current Intros") - .push_builder(intro_list( - intros, - "Remove Intro", - &format!( - "{}/v2/intros/remove/{}/{}", - state.origin, guild_id, channel_name - ), - )) - }, - ) - }); + for (_, intros) in user_intros { + for (channel_name, intros) in + intros.group_by(|intro| &intro.channel_name).into_iter() + { + b = b.builder(Tag::Article, |b| { + b.builder_text(Tag::Header, &channel_name).builder( + Tag::Div, + |b| { + b.attribute("id", "channel-intro-selector") + .push_builder(channel_intro_selector( + &state.origin, + guild_id, + channel_name, + intros.map(|intro| &intro.intro), + guild_intros.iter(), + )) + }, + ) + }); + } } - // for (channel_name, channel_settings) in &guild.channels { - // if let Some(channel_user) = channel_settings.users.get(&user.name) { - // let current_intros = - // channel_user.intros.iter().filter_map(|intro_index| { - // Some(( - // &intro_index.index, - // guild.intros.get(&intro_index.index)?, - // )) - // }); - // let available_intros = - // guild.intros.iter().filter_map(|intro| { - // if !channel_user - // .intros - // .iter() - // .any(|intro_index| intro.0 == &intro_index.index) - // { - // Some((intro.0, intro.1)) - // } else { - // None - // } - // }); - // b = b.builder(Tag::Article, |b| { - // b.builder_text(Tag::Header, channel_name).builder( - // Tag::Div, - // |b| { - // b.builder_text(Tag::Strong, "Your Current Intros") - // .push_builder(intro_list( - // current_intros, - // "Remove Intro", - // &format!( - // "{}/v2/intros/remove/{}/{}", - // state.origin, guild_id, channel_name - // ), - // )) - // .builder_text(Tag::Strong, "Select Intros") - // .push_builder(intro_list( - // available_intros, - // "Add Intro", - // &format!( - // "{}/v2/intros/add/{}/{}", - // state.origin, guild_id, channel_name - // ), - // )) - // }, - // ) - // }); - // } - // } - b }) }) @@ -244,6 +205,28 @@ pub(crate) async fn guild_dashboard( )) } +pub fn channel_intro_selector<'a>( + origin: &str, + guild_id: u64, + channel_name: &String, + intros: impl Iterator, + guild_intros: impl Iterator, +) -> HtmxBuilder { + HtmxBuilder::new(Tag::Empty) + .builder_text(Tag::Strong, "Your Current Intros") + .push_builder(intro_list( + intros, + "Remove Intro", + &format!("{}/v2/intros/remove/{}/{}", origin, guild_id, &channel_name), + )) + .builder_text(Tag::Strong, "Select Intros") + .push_builder(intro_list( + guild_intros, + "Add Intro", + &format!("{}/v2/intros/add/{}/{}", origin, guild_id, channel_name), + )) +} + fn upload_form(origin: &str, guild_id: u64) -> HtmxBuilder { HtmxBuilder::new(Tag::Empty).form(|b| { b.attribute("class", "container") diff --git a/src/routes.rs b/src/routes.rs index b6e927e..59ec973 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -4,11 +4,12 @@ use axum::{ body::Bytes, extract::{Multipart, Path, Query, State}, http::{HeaderMap, HeaderValue}, - response::{IntoResponse, Redirect}, + response::{Html, IntoResponse, Redirect}, Form, Json, }; use axum_extra::extract::{cookie::Cookie, CookieJar}; +use iter_tools::Itertools; use reqwest::{Proxy, StatusCode, Url}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -17,6 +18,8 @@ use uuid::Uuid; use crate::{ auth::{self, User}, + htmx::Build, + page, settings::FileIntro, }; use crate::{ @@ -89,6 +92,9 @@ pub(crate) enum Error { YtdlTerminated, #[error("ffmpeg terminated unsuccessfully")] FfmpegTerminated, + + #[error("database error: {0}")] + Database(#[from] rusqlite::Error), } impl IntoResponse for Error { @@ -111,6 +117,10 @@ impl IntoResponse for Error { Self::YtdlTerminated | Self::FfmpegTerminated => { (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() } + + Self::Database(error) => { + (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response() + } } } } @@ -328,45 +338,72 @@ pub(crate) async fn v2_add_intro_to_user( Path((guild_id, channel)): Path<(u64, String)>, user: User, mut form_data: Multipart, -) -> HeaderMap { - let mut headers = HeaderMap::new(); - headers.insert("HX-Refresh", HeaderValue::from_static("true")); - - let mut settings = state.settings.lock().await; - - let Some(guild) = settings.guilds.get_mut(&guild_id) else { - return headers; - }; - let Some(channel) = guild.channels.get_mut(&channel) else { - return headers; - }; - let Some(channel_user) = channel.users.get_mut(&user.name) else { - return headers; - }; +) -> Result, Redirect> { + let db = state.db.lock().await; while let Ok(Some(field)) = form_data.next_field().await { let Some(field_name) = field.name() else { continue; }; - if !channel_user - .intros - .iter() - .any(|intro| intro.index == field_name) - { - channel_user.intros.push(IntroIndex { - index: field_name.to_string(), - volume: 20, - }); - } + // TODO: insert into database + //if !channel_user + // .intros + // .iter() + // .any(|intro| intro.index == field_name) + //{ + // channel_user.intros.push(IntroIndex { + // index: field_name.to_string(), + // volume: 20, + // }); + //} } - // TODO: don't save on every change - if let Err(err) = settings.save() { - error!("Failed to save config: {err:?}"); - } + let guild_intros = db.get_guild_intros(guild_id).map_err(|err| { + error!(?err, %guild_id, "couldn't get guild intros"); + // TODO: change to actual error + Redirect::to("/login") + })?; + let all_user_intros = db.get_all_user_intros(guild_id).map_err(|err| { + error!(?err, %guild_id, "couldn't get user intros"); + // TODO: change to actual error + Redirect::to("/login") + })?; - headers + let grouped_intros = all_user_intros.iter().group_by(|intro| &intro.username); + let user_intros = grouped_intros + .into_iter() + .filter_map(|(username, intro)| { + if username == &user.name { + Some(intro) + } else { + None + } + }) + .flatten(); + + let grouped_user_intros = user_intros.group_by(|intro| &intro.channel_name); + let intros = grouped_user_intros + .into_iter() + .filter_map(|(channel_name, intros)| { + if channel_name == &channel { + Some(intros.map(|intro| &intro.intro)) + } else { + None + } + }) + .flatten(); + + Ok(Html( + page::channel_intro_selector( + &state.origin, + guild_id, + &channel, + intros, + guild_intros.iter(), + ) + .build(), + )) } pub(crate) async fn v2_remove_intro_from_user(