Compare commits
	
		
			No commits in common. "52d7cc7dedc6d27c867bf953fb781a81fc13e730" and "969a97cab783b7f1d00e786bdc99d5cf1f3e775f" have entirely different histories. 
		
	
	
		
			52d7cc7ded
			...
			969a97cab7
		
	
		|  | @ -492,12 +492,6 @@ version = "0.15.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" | ||||||
| 
 | 
 | ||||||
| [[package]] |  | ||||||
| name = "either" |  | ||||||
| version = "1.9.0" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" |  | ||||||
| 
 |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "encoding_rs" | name = "encoding_rs" | ||||||
| version = "0.8.32" | version = "0.8.32" | ||||||
|  | @ -973,24 +967,6 @@ version = "2.8.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" | 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]] | [[package]] | ||||||
| name = "itoa" | name = "itoa" | ||||||
| version = "1.0.9" | version = "1.0.9" | ||||||
|  | @ -1097,7 +1073,6 @@ dependencies = [ | ||||||
|  "chrono", |  "chrono", | ||||||
|  "dotenv", |  "dotenv", | ||||||
|  "futures", |  "futures", | ||||||
|  "iter_tools", |  | ||||||
|  "reqwest", |  "reqwest", | ||||||
|  "rusqlite", |  "rusqlite", | ||||||
|  "serde", |  "serde", | ||||||
|  |  | ||||||
|  | @ -12,7 +12,6 @@ axum-extra = { version = "0.7.5", features = ["cookie-private", "cookie"] } | ||||||
| chrono = "0.4.23" | chrono = "0.4.23" | ||||||
| dotenv = "0.15.0" | dotenv = "0.15.0" | ||||||
| futures = "0.3.26" | futures = "0.3.26" | ||||||
| iter_tools = "0.1.4" |  | ||||||
| reqwest = "0.11.14" | reqwest = "0.11.14" | ||||||
| rusqlite = { version = "0.29.0", features = ["bundled"] } | rusqlite = { version = "0.29.0", features = ["bundled"] } | ||||||
| serde = "1.0.152" | serde = "1.0.152" | ||||||
|  |  | ||||||
							
								
								
									
										207
									
								
								src/db.rs
								
								
								
								
							
							
						
						
									
										207
									
								
								src/db.rs
								
								
								
								
							|  | @ -1,10 +1,6 @@ | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
| 
 | 
 | ||||||
| use iter_tools::Itertools; |  | ||||||
| use rusqlite::{Connection, Result}; | use rusqlite::{Connection, Result}; | ||||||
| use tracing::{error, warn}; |  | ||||||
| 
 |  | ||||||
| use crate::auth; |  | ||||||
| 
 | 
 | ||||||
| pub struct Database { | pub struct Database { | ||||||
|     conn: Connection, |     conn: Connection, | ||||||
|  | @ -21,7 +17,7 @@ impl Database { | ||||||
|         let mut query = self.conn.prepare( |         let mut query = self.conn.prepare( | ||||||
|             " |             " | ||||||
|             SELECT |             SELECT | ||||||
|                 id, name, soundDelay |                 id, name, sound_delay | ||||||
|             FROM Guild |             FROM Guild | ||||||
|             LEFT JOIN UserGuild ON UserGuild.guild_id = Guild.id |             LEFT JOIN UserGuild ON UserGuild.guild_id = Guild.id | ||||||
|             WHERE UserGuild.username = :username |             WHERE UserGuild.username = :username | ||||||
|  | @ -43,196 +39,6 @@ impl Database { | ||||||
| 
 | 
 | ||||||
|         guilds |         guilds | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     pub fn get_guild_intros(&self, guild_id: u64) -> Result<Vec<Intro>> { |  | ||||||
|         let mut query = self.conn.prepare( |  | ||||||
|             " |  | ||||||
|             SELECT |  | ||||||
|                 Intro.id, |  | ||||||
|                 Intro.name |  | ||||||
|             FROM Intro |  | ||||||
|             WHERE |  | ||||||
|                 Intro.guild_id = :guild_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(Intro { |  | ||||||
|                         id: row.get(0)?, |  | ||||||
|                         name: row.get(1)?, |  | ||||||
|                     }) |  | ||||||
|                 }, |  | ||||||
|             )? |  | ||||||
|             .into_iter() |  | ||||||
|             .collect::<Result<Vec<Intro>>>(); |  | ||||||
| 
 |  | ||||||
|         intros |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_all_user_intros(&self, guild_id: u64) -> Result<Vec<UserIntro>> { |  | ||||||
|         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::<Result<Vec<UserIntro>>>(); |  | ||||||
| 
 |  | ||||||
|         intros |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub(crate) fn get_user_permissions( |  | ||||||
|         &self, |  | ||||||
|         username: &str, |  | ||||||
|         guild_id: u64, |  | ||||||
|     ) -> Result<auth::Permissions> { |  | ||||||
|         self.conn.query_row( |  | ||||||
|             " |  | ||||||
|             SELECT |  | ||||||
|                 permissions |  | ||||||
|             FROM UserPermission |  | ||||||
|             WHERE |  | ||||||
|                 username = ?1 |  | ||||||
|             ",
 |  | ||||||
|             [username], |  | ||||||
|             |row| Ok(auth::Permissions(row.get(0)?)), |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub(crate) fn get_guild_channels(&self, guild_id: u64) -> Result<Vec<String>> { |  | ||||||
|         let mut query = self.conn.prepare( |  | ||||||
|             " |  | ||||||
|             SELECT |  | ||||||
|                 Channel.name |  | ||||||
|             FROM Channel |  | ||||||
|             WHERE |  | ||||||
|                 Channel.guild_id = :guild_id |  | ||||||
|             ORDER BY Channel.name DESC |  | ||||||
|             ",
 |  | ||||||
|         )?; |  | ||||||
| 
 |  | ||||||
|         // 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(row.get(0)?), |  | ||||||
|             )? |  | ||||||
|             .into_iter() |  | ||||||
|             .collect::<Result<Vec<String>>>(); |  | ||||||
| 
 |  | ||||||
|         intros |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub(crate) fn get_user_channel_intros( |  | ||||||
|         &self, |  | ||||||
|         username: &str, |  | ||||||
|         guild_id: u64, |  | ||||||
|         channel_name: &str, |  | ||||||
|     ) -> Result<Vec<Intro>> { |  | ||||||
|         let all_user_intros = self.get_all_user_intros(guild_id)?.into_iter(); |  | ||||||
| 
 |  | ||||||
|         let intros = all_user_intros |  | ||||||
|             .filter(|intro| &intro.username == &username && &intro.channel_name == channel_name) |  | ||||||
|             .map(|intro| intro.intro) |  | ||||||
|             .collect(); |  | ||||||
| 
 |  | ||||||
|         Ok(intros) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn insert_user_intro( |  | ||||||
|         &self, |  | ||||||
|         username: &str, |  | ||||||
|         guild_id: u64, |  | ||||||
|         channel_name: &str, |  | ||||||
|         intro_id: i32, |  | ||||||
|     ) -> Result<()> { |  | ||||||
|         let affected = self.conn.execute( |  | ||||||
|             "INSERT INTO UserIntro (username, guild_id, channel_name, intro_id) VALUES (?1, ?2, ?3, ?4)", |  | ||||||
|             &[ |  | ||||||
|                 username, |  | ||||||
|                 &guild_id.to_string(), |  | ||||||
|                 channel_name, |  | ||||||
|                 &intro_id.to_string(), |  | ||||||
|             ], |  | ||||||
|         )?; |  | ||||||
| 
 |  | ||||||
|         if affected < 1 { |  | ||||||
|             warn!("no rows affected when attempting to insert user intro"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn remove_user_intro( |  | ||||||
|         &self, |  | ||||||
|         username: &str, |  | ||||||
|         guild_id: u64, |  | ||||||
|         channel_name: &str, |  | ||||||
|         intro_id: i32, |  | ||||||
|     ) -> Result<()> { |  | ||||||
|         let affected = self.conn.execute( |  | ||||||
|             "DELETE FROM
 |  | ||||||
|                 UserIntro |  | ||||||
|             WHERE 
 |  | ||||||
|                 username = ?1 
 |  | ||||||
|             AND guild_id = ?2 
 |  | ||||||
|             AND channel_name = ?3 
 |  | ||||||
|             AND intro_id = ?4",
 |  | ||||||
|             &[ |  | ||||||
|                 username, |  | ||||||
|                 &guild_id.to_string(), |  | ||||||
|                 channel_name, |  | ||||||
|                 &intro_id.to_string(), |  | ||||||
|             ], |  | ||||||
|         )?; |  | ||||||
| 
 |  | ||||||
|         if affected < 1 { |  | ||||||
|             warn!("no rows affected when attempting to delete user intro"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct Guild { | pub struct Guild { | ||||||
|  | @ -240,14 +46,3 @@ pub struct Guild { | ||||||
|     pub name: String, |     pub name: String, | ||||||
|     pub sound_delay: u32, |     pub sound_delay: u32, | ||||||
| } | } | ||||||
| 
 |  | ||||||
| pub struct Intro { |  | ||||||
|     pub id: i32, |  | ||||||
|     pub name: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub struct UserIntro { |  | ||||||
|     pub intro: Intro, |  | ||||||
|     pub channel_name: String, |  | ||||||
|     pub username: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										141
									
								
								src/page.rs
								
								
								
								
							
							
						
						
									
										141
									
								
								src/page.rs
								
								
								
								
							|  | @ -8,7 +8,6 @@ use axum::{ | ||||||
|     extract::{Path, State}, |     extract::{Path, State}, | ||||||
|     response::{Html, Redirect}, |     response::{Html, Redirect}, | ||||||
| }; | }; | ||||||
| use iter_tools::Itertools; |  | ||||||
| use tracing::error; | use tracing::error; | ||||||
| 
 | 
 | ||||||
| fn page_header(title: &str) -> HtmxBuilder { | fn page_header(title: &str) -> HtmxBuilder { | ||||||
|  | @ -70,14 +69,13 @@ fn guild_list<'a>(origin: &str, guilds: impl Iterator<Item = &'a db::Guild>) -> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn intro_list<'a>( | fn intro_list<'a>( | ||||||
|     intros: impl Iterator<Item = &'a db::Intro>, |     intros: impl Iterator<Item = (&'a String, &'a Intro)>, | ||||||
|     label: &str, |     label: &str, | ||||||
|     post: &str, |     post: &str, | ||||||
| ) -> HtmxBuilder { | ) -> HtmxBuilder { | ||||||
|     HtmxBuilder::new(Tag::Empty).form(|b| { |     HtmxBuilder::new(Tag::Empty).form(|b| { | ||||||
|         b.attribute("class", "container") |         b.attribute("class", "container") | ||||||
|             .hx_post(post) |             .hx_post(post) | ||||||
|             .hx_target("closest #channel-intro-selector") |  | ||||||
|             .attribute("hx-encoding", "multipart/form-data") |             .attribute("hx-encoding", "multipart/form-data") | ||||||
|             .builder(Tag::FieldSet, |b| { |             .builder(Tag::FieldSet, |b| { | ||||||
|                 let mut b = b |                 let mut b = b | ||||||
|  | @ -86,10 +84,9 @@ fn intro_list<'a>( | ||||||
|                 for intro in intros { |                 for intro in intros { | ||||||
|                     b = b.builder(Tag::Label, |b| { |                     b = b.builder(Tag::Label, |b| { | ||||||
|                         b.builder(Tag::Input, |b| { |                         b.builder(Tag::Input, |b| { | ||||||
|                             b.attribute("type", "checkbox") |                             b.attribute("type", "checkbox").attribute("name", &intro.0) | ||||||
|                                 .attribute("name", &intro.id.to_string()) |  | ||||||
|                         }) |                         }) | ||||||
|                         .builder_text(Tag::Paragraph, &intro.name) |                         .builder_text(Tag::Paragraph, intro.1.friendly_name()) | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  | @ -104,34 +101,19 @@ pub(crate) async fn guild_dashboard( | ||||||
|     user: User, |     user: User, | ||||||
|     Path(guild_id): Path<u64>, |     Path(guild_id): Path<u64>, | ||||||
| ) -> Result<Html<String>, Redirect> { | ) -> Result<Html<String>, Redirect> { | ||||||
|     let db = state.db.lock().await; |     let settings = state.settings.lock().await; | ||||||
| 
 | 
 | ||||||
|     let guild_intros = db.get_guild_intros(guild_id).map_err(|err| { |     let Some(guild) = settings.guilds.get(&guild_id) else { | ||||||
|         error!(?err, %guild_id, "couldn't get guild intros"); |         error!(%guild_id, "no such guild"); | ||||||
|         // TODO: change to actual error
 |         return Err(Redirect::to(&format!("{}/", state.origin))); | ||||||
|         Redirect::to("/login") |     }; | ||||||
|     })?; |     let Some(guild_user) = guild.users.get(&user.name) else { | ||||||
|     let guild_channels = db.get_guild_channels(guild_id).map_err(|err| { |         error!(%guild_id, %user.name, "no user in guild"); | ||||||
|         error!(?err, %guild_id, "couldn't get guild channels"); |         return Err(Redirect::to(&format!("{}/", state.origin))); | ||||||
|         // 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 user_intros = all_user_intros |     let can_upload = guild_user.permissions.can(auth::Permission::UploadSounds); | ||||||
|         .iter() |     let is_moderator = guild_user.permissions.can(auth::Permission::DeleteSounds); | ||||||
|         .filter(|intro| &intro.username == &user.name) |  | ||||||
|         .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( |     Ok(Html( | ||||||
|         HtmxBuilder::new(Tag::Html) |         HtmxBuilder::new(Tag::Html) | ||||||
|  | @ -180,32 +162,53 @@ pub(crate) async fn guild_dashboard( | ||||||
|                         .builder(Tag::Article, |b| { |                         .builder(Tag::Article, |b| { | ||||||
|                             let mut b = b.builder_text(Tag::Header, "Guild Intros"); |                             let mut b = b.builder_text(Tag::Header, "Guild Intros"); | ||||||
| 
 | 
 | ||||||
|                             let mut user_intros = user_intros.into_iter().peekable(); |                             for (channel_name, channel_settings) in &guild.channels { | ||||||
| 
 |                                 if let Some(channel_user) = channel_settings.users.get(&user.name) { | ||||||
|                             for guild_channel_name in guild_channels { |                                     let current_intros = | ||||||
|                                 // Get user intros for this channel
 |                                         channel_user.intros.iter().filter_map(|intro_index| { | ||||||
|                                 let intros = user_intros |                                             Some(( | ||||||
|                                     .peeking_take_while(|(channel_name, _)| { |                                                 &intro_index.index, | ||||||
|                                         channel_name == &&guild_channel_name |                                                 guild.intros.get(&intro_index.index)?, | ||||||
|                                     }) |                                             )) | ||||||
|                                     .map(|(_, intros)| intros.map(|intro| &intro.intro)) |                                         }); | ||||||
|                                     .flatten(); |                                     let available_intros = | ||||||
| 
 |                                         guild.intros.iter().filter_map(|intro| { | ||||||
|                                 b = b.builder(Tag::Article, |b| { |                                             if !channel_user | ||||||
|                                     b.builder_text(Tag::Header, &guild_channel_name).builder( |                                                 .intros | ||||||
|                                         Tag::Div, |                                                 .iter() | ||||||
|                                         |b| { |                                                 .any(|intro_index| intro.0 == &intro_index.index) | ||||||
|                                             b.attribute("id", "channel-intro-selector") |                                             { | ||||||
|                                                 .push_builder(channel_intro_selector( |                                                 Some((intro.0, intro.1)) | ||||||
|                                                     &state.origin, |                                             } else { | ||||||
|                                                     guild_id, |                                                 None | ||||||
|                                                     &guild_channel_name, |                                             } | ||||||
|                                                     intros, |                                         }); | ||||||
|                                                     guild_intros.iter(), |                                     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 |                             b | ||||||
|  | @ -216,28 +219,6 @@ pub(crate) async fn guild_dashboard( | ||||||
|     )) |     )) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub fn channel_intro_selector<'a>( |  | ||||||
|     origin: &str, |  | ||||||
|     guild_id: u64, |  | ||||||
|     channel_name: &String, |  | ||||||
|     intros: impl Iterator<Item = &'a db::Intro>, |  | ||||||
|     guild_intros: impl Iterator<Item = &'a db::Intro>, |  | ||||||
| ) -> 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 { | fn upload_form(origin: &str, guild_id: u64) -> HtmxBuilder { | ||||||
|     HtmxBuilder::new(Tag::Empty).form(|b| { |     HtmxBuilder::new(Tag::Empty).form(|b| { | ||||||
|         b.attribute("class", "container") |         b.attribute("class", "container") | ||||||
|  |  | ||||||
							
								
								
									
										147
									
								
								src/routes.rs
								
								
								
								
							
							
						
						
									
										147
									
								
								src/routes.rs
								
								
								
								
							|  | @ -4,12 +4,11 @@ use axum::{ | ||||||
|     body::Bytes, |     body::Bytes, | ||||||
|     extract::{Multipart, Path, Query, State}, |     extract::{Multipart, Path, Query, State}, | ||||||
|     http::{HeaderMap, HeaderValue}, |     http::{HeaderMap, HeaderValue}, | ||||||
|     response::{Html, IntoResponse, Redirect}, |     response::{IntoResponse, Redirect}, | ||||||
|     Form, Json, |     Form, Json, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use axum_extra::extract::{cookie::Cookie, CookieJar}; | use axum_extra::extract::{cookie::Cookie, CookieJar}; | ||||||
| use iter_tools::Itertools; |  | ||||||
| use reqwest::{Proxy, StatusCode, Url}; | use reqwest::{Proxy, StatusCode, Url}; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use serde_json::{json, Value}; | use serde_json::{json, Value}; | ||||||
|  | @ -18,8 +17,6 @@ use uuid::Uuid; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     auth::{self, User}, |     auth::{self, User}, | ||||||
|     htmx::Build, |  | ||||||
|     page, |  | ||||||
|     settings::FileIntro, |     settings::FileIntro, | ||||||
| }; | }; | ||||||
| use crate::{ | use crate::{ | ||||||
|  | @ -92,9 +89,6 @@ pub(crate) enum Error { | ||||||
|     YtdlTerminated, |     YtdlTerminated, | ||||||
|     #[error("ffmpeg terminated unsuccessfully")] |     #[error("ffmpeg terminated unsuccessfully")] | ||||||
|     FfmpegTerminated, |     FfmpegTerminated, | ||||||
| 
 |  | ||||||
|     #[error("database error: {0}")] |  | ||||||
|     Database(#[from] rusqlite::Error), |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl IntoResponse for Error { | impl IntoResponse for Error { | ||||||
|  | @ -117,10 +111,6 @@ impl IntoResponse for Error { | ||||||
|             Self::YtdlTerminated | Self::FfmpegTerminated => { |             Self::YtdlTerminated | Self::FfmpegTerminated => { | ||||||
|                 (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() |                 (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|             Self::Database(error) => { |  | ||||||
|                 (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response() |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -338,52 +328,45 @@ pub(crate) async fn v2_add_intro_to_user( | ||||||
|     Path((guild_id, channel)): Path<(u64, String)>, |     Path((guild_id, channel)): Path<(u64, String)>, | ||||||
|     user: User, |     user: User, | ||||||
|     mut form_data: Multipart, |     mut form_data: Multipart, | ||||||
| ) -> Result<Html<String>, Redirect> { | ) -> HeaderMap { | ||||||
|     let db = state.db.lock().await; |     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; | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     while let Ok(Some(field)) = form_data.next_field().await { |     while let Ok(Some(field)) = form_data.next_field().await { | ||||||
|         let Some(intro_id) = field.name() else { |         let Some(field_name) = field.name() else { | ||||||
|             continue; |             continue; | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let intro_id = intro_id.parse::<i32>().map_err(|err| { |         if !channel_user | ||||||
|             error!(?err, "invalid intro id"); |             .intros | ||||||
|             // TODO: change to actual error
 |             .iter() | ||||||
|             Redirect::to("/login") |             .any(|intro| intro.index == field_name) | ||||||
|         })?; |         { | ||||||
| 
 |             channel_user.intros.push(IntroIndex { | ||||||
|         db.insert_user_intro(&user.name, guild_id, &channel, intro_id) |                 index: field_name.to_string(), | ||||||
|             .map_err(|err| { |                 volume: 20, | ||||||
|                 error!(?err, "failed to add user intro"); |             }); | ||||||
|                 // TODO: change to actual error
 |         } | ||||||
|                 Redirect::to("/login") |  | ||||||
|             })?; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let guild_intros = db.get_guild_intros(guild_id).map_err(|err| { |     // TODO: don't save on every change
 | ||||||
|         error!(?err, %guild_id, "couldn't get guild intros"); |     if let Err(err) = settings.save() { | ||||||
|         // TODO: change to actual error
 |         error!("Failed to save config: {err:?}"); | ||||||
|         Redirect::to("/login") |     } | ||||||
|     })?; |  | ||||||
| 
 | 
 | ||||||
|     let intros = db |     headers | ||||||
|         .get_user_channel_intros(&user.name, guild_id, &channel) |  | ||||||
|         .map_err(|err| { |  | ||||||
|             error!(?err, user = %user.name, %guild_id, "couldn't get user intros"); |  | ||||||
|             // TODO: change to actual error
 |  | ||||||
|             Redirect::to("/login") |  | ||||||
|         })?; |  | ||||||
| 
 |  | ||||||
|     Ok(Html( |  | ||||||
|         page::channel_intro_selector( |  | ||||||
|             &state.origin, |  | ||||||
|             guild_id, |  | ||||||
|             &channel, |  | ||||||
|             intros.iter(), |  | ||||||
|             guild_intros.iter(), |  | ||||||
|         ) |  | ||||||
|         .build(), |  | ||||||
|     )) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub(crate) async fn v2_remove_intro_from_user( | pub(crate) async fn v2_remove_intro_from_user( | ||||||
|  | @ -391,52 +374,42 @@ pub(crate) async fn v2_remove_intro_from_user( | ||||||
|     Path((guild_id, channel)): Path<(u64, String)>, |     Path((guild_id, channel)): Path<(u64, String)>, | ||||||
|     user: User, |     user: User, | ||||||
|     mut form_data: Multipart, |     mut form_data: Multipart, | ||||||
| ) -> Result<Html<String>, Redirect> { | ) -> HeaderMap { | ||||||
|     let db = state.db.lock().await; |     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; | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     while let Ok(Some(field)) = form_data.next_field().await { |     while let Ok(Some(field)) = form_data.next_field().await { | ||||||
|         let Some(intro_id) = field.name() else { |         let Some(field_name) = field.name() else { | ||||||
|             continue; |             continue; | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let intro_id = intro_id.parse::<i32>().map_err(|err| { |         if let Some(index) = channel_user | ||||||
|             error!(?err, "invalid intro id"); |             .intros | ||||||
|             // TODO: change to actual error
 |             .iter() | ||||||
|             Redirect::to("/login") |             .position(|intro| intro.index == field_name) | ||||||
|         })?; |         { | ||||||
| 
 |             channel_user.intros.remove(index); | ||||||
|         db.remove_user_intro(&user.name, guild_id, &channel, intro_id) |         } | ||||||
|             .map_err(|err| { |  | ||||||
|                 error!(?err, "failed to remove user intro"); |  | ||||||
|                 // TODO: change to actual error
 |  | ||||||
|                 Redirect::to("/login") |  | ||||||
|             })?; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let guild_intros = db.get_guild_intros(guild_id).map_err(|err| { |     // TODO: don't save on every change
 | ||||||
|         error!(?err, %guild_id, "couldn't get guild intros"); |     if let Err(err) = settings.save() { | ||||||
|         // TODO: change to actual error
 |         error!("Failed to save config: {err:?}"); | ||||||
|         Redirect::to("/login") |     } | ||||||
|     })?; |  | ||||||
| 
 | 
 | ||||||
|     let intros = db |     headers | ||||||
|         .get_user_channel_intros(&user.name, guild_id, &channel) |  | ||||||
|         .map_err(|err| { |  | ||||||
|             error!(?err, user = %user.name, %guild_id, "couldn't get user intros"); |  | ||||||
|             // TODO: change to actual error
 |  | ||||||
|             Redirect::to("/login") |  | ||||||
|         })?; |  | ||||||
| 
 |  | ||||||
|     Ok(Html( |  | ||||||
|         page::channel_intro_selector( |  | ||||||
|             &state.origin, |  | ||||||
|             guild_id, |  | ||||||
|             &channel, |  | ||||||
|             intros.iter(), |  | ||||||
|             guild_intros.iter(), |  | ||||||
|         ) |  | ||||||
|         .build(), |  | ||||||
|     )) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub(crate) async fn add_intro_to_user( | pub(crate) async fn add_intro_to_user( | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue