diff --git a/Cargo.lock b/Cargo.lock index ae7a8f3..04daab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,6 +492,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -967,6 +973,24 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "iter_tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531cafdc99b3b3252bb32f5620e61d56b19415efc19900b12d1b2e7483854897" +dependencies = [ + "itertools", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1073,6 +1097,7 @@ dependencies = [ "chrono", "dotenv", "futures", + "iter_tools", "reqwest", "rusqlite", "serde", diff --git a/Cargo.toml b/Cargo.toml index 76f034c..2276373 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ axum-extra = { version = "0.7.5", features = ["cookie-private", "cookie"] } chrono = "0.4.23" dotenv = "0.15.0" futures = "0.3.26" +iter_tools = "0.1.4" reqwest = "0.11.14" rusqlite = { version = "0.29.0", features = ["bundled"] } serde = "1.0.152" diff --git a/src/db.rs b/src/db.rs index 2c14665..6224db0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2,6 +2,8 @@ use std::path::Path; use rusqlite::{Connection, Result}; +use crate::auth; + pub struct Database { conn: Connection, } @@ -17,7 +19,7 @@ impl Database { let mut query = self.conn.prepare( " SELECT - id, name, sound_delay + id, name, soundDelay FROM Guild LEFT JOIN UserGuild ON UserGuild.guild_id = Guild.id WHERE UserGuild.username = :username @@ -39,6 +41,63 @@ impl Database { guilds } + + pub fn get_all_user_intros(&self, username: &str, guild_id: u64) -> Result> { + let mut query = self.conn.prepare( + " + SELECT + Intro.id, + Intro.name, + UI.channel_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; + ", + )?; + + // 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( + &[ + (":username", username), + // :vomit: + (":guild_id", &guild_id.to_string()), + ], + |row| { + Ok(Intro { + id: row.get(0)?, + name: row.get(1)?, + channel_name: row.get(2)?, + }) + }, + )? + .into_iter() + .collect::>>(); + + intros + } + + pub(crate) fn get_user_permissions( + &self, + username: &str, + guild_id: u64, + ) -> Result { + self.conn.query_row( + " + SELECT + permissions + FROM UserPermission + WHERE + username = ?1 + ", + [username], + |row| Ok(auth::Permissions(row.get(0)?)), + ) + } } pub struct Guild { @@ -46,3 +105,9 @@ pub struct Guild { pub name: String, pub sound_delay: u32, } + +pub struct Intro { + pub id: i32, + pub name: String, + pub channel_name: String, +} diff --git a/src/page.rs b/src/page.rs index e0e4eec..ac5943c 100644 --- a/src/page.rs +++ b/src/page.rs @@ -8,6 +8,7 @@ use axum::{ extract::{Path, State}, response::{Html, Redirect}, }; +use iter_tools::Itertools; use tracing::error; fn page_header(title: &str) -> HtmxBuilder { @@ -69,7 +70,7 @@ fn guild_list<'a>(origin: &str, guilds: impl Iterator) -> } fn intro_list<'a>( - intros: impl Iterator, + intros: impl Iterator, label: &str, post: &str, ) -> HtmxBuilder { @@ -84,9 +85,10 @@ fn intro_list<'a>( for intro in intros { b = b.builder(Tag::Label, |b| { b.builder(Tag::Input, |b| { - b.attribute("type", "checkbox").attribute("name", &intro.0) + b.attribute("type", "checkbox") + .attribute("name", &intro.id.to_string()) }) - .builder_text(Tag::Paragraph, intro.1.friendly_name()) + .builder_text(Tag::Paragraph, &intro.name) }); } @@ -101,19 +103,23 @@ pub(crate) async fn guild_dashboard( user: User, Path(guild_id): Path, ) -> Result, Redirect> { - let settings = state.settings.lock().await; + let db = state.db.lock().await; - let Some(guild) = settings.guilds.get(&guild_id) else { - error!(%guild_id, "no such guild"); - return Err(Redirect::to(&format!("{}/", state.origin))); - }; - let Some(guild_user) = guild.users.get(&user.name) else { - error!(%guild_id, %user.name, "no user in guild"); - return Err(Redirect::to(&format!("{}/", state.origin))); - }; + 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 user_permissions = db + .get_user_permissions(&user.name, guild_id) + .unwrap_or_default(); - let can_upload = guild_user.permissions.can(auth::Permission::UploadSounds); - let is_moderator = guild_user.permissions.can(auth::Permission::DeleteSounds); + let channel_user_intros = user_intros.iter().group_by(|intro| &intro.channel_name); + + let can_upload = user_permissions.can(auth::Permission::UploadSounds); + let is_moderator = user_permissions.can(auth::Permission::DeleteSounds); Ok(Html( HtmxBuilder::new(Tag::Html) @@ -162,55 +168,74 @@ pub(crate) async fn guild_dashboard( .builder(Tag::Article, |b| { let mut b = b.builder_text(Tag::Header, "Guild Intros"); - 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 - ), - )) - }, - ) - }); - } + 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 (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 }) })