diff --git a/.gitignore b/.gitignore index fd9eebb..8085ce7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /target /config +/sounds /.idea .DS_Store diff --git a/src/auth.rs b/src/auth.rs index 9e2dc17..4a84c45 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -18,6 +18,7 @@ pub(crate) struct Discord { pub(crate) struct DiscordSecret { pub(crate) client_id: String, pub(crate) client_secret: String, + pub(crate) bot_token: String, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -105,6 +106,7 @@ pub(crate) enum Permission { UploadSounds = 1, DeleteSounds = 2, Soundboard = 4, + AddChannel = 8, Moderator = 128, } @@ -121,6 +123,7 @@ impl ToString for Permission { Permission::UploadSounds => "Upload Sounds".to_string(), Permission::DeleteSounds => "Delete Sounds".to_string(), Permission::Soundboard => "Soundboard".to_string(), + Permission::AddChannel => "Add Channel".to_string(), Permission::Moderator => "Moderator".to_string(), } } @@ -134,6 +137,7 @@ impl FromStr for Permission { "Upload Sounds" => Ok(Self::UploadSounds), "Delete Sounds" => Ok(Self::DeleteSounds), "Soundboard" => Ok(Self::Soundboard), + "Add Channel" => Ok(Self::AddChannel), "Moderator" => Ok(Self::Moderator), _ => Err(Self::Err::InvalidRequest), } diff --git a/src/db/mod.rs b/src/db/mod.rs index 31c940f..b4b9ec3 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -341,6 +341,21 @@ impl Database { Ok(()) } + pub fn insert_guild_channel(&self, guild_id: &u64, name: &str) -> Result<()> { + let affected = self.conn.execute( + "INSERT INTO + Channel (name, guild_id) + VALUES (?1, ?2)", + [name.to_string(), guild_id.to_string()], + )?; + + if affected < 1 { + warn!("no rows affected when attempting to insert channel"); + } + + Ok(()) + } + pub fn insert_user( &self, username: &str, diff --git a/src/main.rs b/src/main.rs index 3595234..baee7a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,6 +121,7 @@ fn spawn_api(db: Arc>) { client_id: env::var("DISCORD_CLIENT_ID").expect("expected DISCORD_CLIENT_ID env var"), client_secret: env::var("DISCORD_CLIENT_SECRET") .expect("expected DISCORD_CLIENT_SECRET env var"), + bot_token: env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var"), }; let origin = env::var("APP_ORIGIN").expect("expected APP_ORIGIN"); @@ -136,7 +137,11 @@ fn spawn_api(db: Arc>) { .route("/index.html", get(page::home)) .route("/login", get(page::login)) .route("/guild/:guild_id", get(page::guild_dashboard)) - .route("/guild/:guild_id/setup", get(page::guild_setup)) + .route("/guild/:guild_id/setup", get(routes::guild_setup)) + .route( + "/guild/:guild_id/add_channel", + post(routes::guild_add_channel), + ) .route( "/guild/:guild_id/permissions/update", post(routes::update_guild_permissions), diff --git a/src/page.rs b/src/page.rs index d3292b5..c5efe60 100644 --- a/src/page.rs +++ b/src/page.rs @@ -215,7 +215,7 @@ pub(crate) async fn guild_dashboard( let can_upload = user_permissions.can(auth::Permission::UploadSounds); let is_moderator = user_permissions.can(auth::Permission::Moderator); - let mod_dashboard = moderator_dashboard(&state, guild_id).await; + let mod_dashboard = moderator_dashboard(&state, &state.secrets.bot_token, guild_id).await; let user_intros = all_user_intros .iter() @@ -305,50 +305,6 @@ pub(crate) async fn guild_dashboard( )) } -#[derive(Debug, Deserialize)] -pub(crate) struct GuildSetupParams { - name: String, -} - -pub(crate) async fn guild_setup( - State(state): State, - user: User, - Path(guild_id): Path, - Query(GuildSetupParams { name }): Query, -) -> Result { - let db = state.db.lock().await; - - let user_permissions = db.get_user_app_permissions(&user.name).unwrap_or_default(); - if !user_permissions.can(auth::AppPermission::AddGuild) { - return Err(Redirect::to(&state.origin)); - } - - db.insert_guild(&guild_id, &name, 0).map_err(|err| { - error!("failed to insert guild into db: {err}"); - Redirect::to(&state.origin) - })?; - - db.insert_user_guild(&user.name, guild_id).map_err(|err| { - error!("failed to insert user guild into db: {err}"); - Redirect::to(&state.origin) - })?; - - db.insert_user_permission( - &user.name, - guild_id, - auth::Permissions(auth::Permission::all()), - ) - .map_err(|err| { - error!("failed to insert user permissions into db: {err}"); - Redirect::to(&state.origin) - })?; - - Ok(Redirect::to(&format!( - "{}/guild/{}", - state.origin, guild_id - ))) -} - pub fn channel_intro_selector<'a>( origin: &str, guild_id: u64, @@ -414,6 +370,83 @@ fn ytdl_form(origin: &str, guild_id: u64) -> HtmxBuilder { }) } +async fn moderator_dashboard(state: &ApiState, bot_token: &str, guild_id: u64) -> HtmxBuilder { + let permissions_editor = permissions_editor(state, guild_id).await; + let channel_editor = channel_editor(state, bot_token, guild_id).await; + HtmxBuilder::new(Tag::Empty) + .push_builder(permissions_editor) + .push_builder(channel_editor) +} + +async fn channel_editor(state: &ApiState, bot_token: &str, guild_id: u64) -> HtmxBuilder { + let db = state.db.lock().await; + let added_guild_channels = db.get_guild_channels(guild_id).unwrap_or_default(); + + let mut got_channels = true; + let client = reqwest::Client::new(); + let channels: Vec = { + match client + .get(format!( + "https://discord.com/api/v10/guilds/{}/channels", + guild_id + )) + .header("Authorization", format!("Bot {}", bot_token)) + .send() + .await + { + Ok(resp) => match resp.json::>().await { + Ok(channels) => channels + .into_iter() + .filter(|channel| channel.ty == crate::routes::ChannelType::GuildVoice as u32) + .filter_map(|channel| channel.name) + .filter(|name| !added_guild_channels.contains(name)) + .collect(), + Err(err) => { + error!(?err, "failed to parse json"); + got_channels = false; + + vec![] + } + }, + Err(err) => { + error!(?err, "failed to get channels"); + got_channels = false; + + vec![] + } + } + }; + + if got_channels && !channels.is_empty() { + HtmxBuilder::new(Tag::Empty).form(|b| { + b.attribute("class", "container") + .hx_post(&format!("{}/guild/{}/add_channel", state.origin, guild_id)) + .attribute("hx-encoding", "multipart/form-data") + .builder(Tag::FieldSet, |b| { + let mut b = b + .attribute("class", "container") + .attribute("style", "max-height: 50%; overflow-y: scroll"); + for channel_name in channels { + b = b.builder(Tag::Label, |b| { + b.builder(Tag::Input, |b| { + b.attribute("type", "checkbox") + .attribute("name", &channel_name.to_string()) + }) + .builder_text(Tag::Paragraph, &channel_name) + }); + } + + b + }) + .button(|b| b.attribute("type", "submit").text("Add Channel")) + }) + } else if channels.is_empty() { + HtmxBuilder::new(Tag::Empty) + } else { + HtmxBuilder::new(Tag::Empty).text("Failed to get channels") + } +} + async fn permissions_editor(state: &ApiState, guild_id: u64) -> HtmxBuilder { let db = state.db.lock().await; let user_permissions = db.get_all_user_permissions(guild_id).unwrap_or_default(); @@ -478,11 +511,6 @@ async fn permissions_editor(state: &ApiState, guild_id: u64) -> HtmxBuilder { }) } -async fn moderator_dashboard(state: &ApiState, guild_id: u64) -> HtmxBuilder { - let permissions_editor = permissions_editor(state, guild_id).await; - HtmxBuilder::new(Tag::Empty).push_builder(permissions_editor) -} - pub(crate) async fn login( State(state): State, user: Option, @@ -490,7 +518,7 @@ pub(crate) async fn login( if user.is_some() { Err(Redirect::to(&format!("{}/", state.origin))) } else { - let authorize_uri = format!("https://discord.com/api/oauth2/authorize?client_id={}&redirect_uri={}/v2/auth&response_type=code&scope=guilds.members.read%20guilds%20identify", state.secrets.client_id, state.origin); + let authorize_uri = format!("https://discord.com/api/oauth2/authorize?client_id={}&redirect_uri={}/v2/auth&response_type=code&scope=guilds.members.read+guilds+identify", state.secrets.client_id, state.origin); Ok(Html( HtmxBuilder::new(Tag::Html) diff --git a/src/routes.rs b/src/routes.rs index 04c46bb..6b9190a 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -94,6 +94,20 @@ pub(crate) struct DiscordUserGuild { pub owner: bool, } +#[derive(Deserialize)] +pub(crate) struct DiscordChannel { + pub name: Option, + #[serde(rename = "type")] + pub ty: u32, +} + +#[derive(Deserialize, PartialEq, Eq)] +#[repr(u32)] +pub(crate) enum ChannelType { + GuildText = 0, + GuildVoice = 2, +} + fn serde_string_as_u64<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -235,7 +249,7 @@ pub(crate) async fn v2_auth( let uri = Url::parse(&state.origin).expect("should be a valid url"); - let mut cookie = Cookie::new("access_token", token.clone()); + let mut cookie = Cookie::new("access_token", token); cookie.set_path(uri.path().to_string()); cookie.set_secure(true); @@ -471,6 +485,67 @@ pub(crate) async fn v2_add_guild_intro( Ok(headers) } +#[derive(Debug, Deserialize)] +pub(crate) struct GuildSetupParams { + name: String, +} + +pub(crate) async fn guild_setup( + State(state): State, + user: db::User, + Path(guild_id): Path, + Query(GuildSetupParams { name }): Query, +) -> Result { + let db = state.db.lock().await; + + let user_permissions = db.get_user_app_permissions(&user.name).unwrap_or_default(); + if !user_permissions.can(auth::AppPermission::AddGuild) { + return Err(Error::InvalidPermission); + } + + db.insert_guild(&guild_id, &name, 0)?; + db.insert_user_guild(&user.name, guild_id)?; + db.insert_user_permission( + &user.name, + guild_id, + auth::Permissions(auth::Permission::all()), + )?; + + Ok(Redirect::to(&format!( + "{}/guild/{}", + state.origin, guild_id + ))) +} + +pub(crate) async fn guild_add_channel( + State(state): State, + user: db::User, + Path(guild_id): Path, + mut form_data: Multipart, +) -> Result { + let db = state.db.lock().await; + + let user_permissions = db + .get_user_permissions(&user.name, guild_id) + .unwrap_or_default(); + if !user_permissions.can(auth::Permission::AddChannel) { + return Err(Error::InvalidPermission); + } + + while let Ok(Some(field)) = form_data.next_field().await { + let Some(channel_name) = field.name() else { + continue; + }; + + db.insert_guild_channel(&guild_id, channel_name)?; + } + + let mut headers = HeaderMap::new(); + headers.insert("HX-Refresh", HeaderValue::from_static("true")); + + Ok(headers) +} + pub(crate) async fn update_guild_permissions( State(state): State, Path(guild_id): Path,