diff --git a/cipher_discord_bot/src/checks.rs b/cipher_discord_bot/src/checks.rs new file mode 100644 index 0000000..2519435 --- /dev/null +++ b/cipher_discord_bot/src/checks.rs @@ -0,0 +1,21 @@ +use cipher_core::repository::staff_role_repository::StaffRoleRepository; +use cipher_core::repository::RepositoryProvider; + +use crate::app::AppData; +use crate::app::AppError; + +pub async fn is_staff(ctx: poise::Context<'_, AppData, AppError>) -> Result> +where + R: RepositoryProvider, +{ + let roles: Vec<_> = match ctx.author_member().await { + Some(member) => member.roles.iter().map(|r| r.get()).collect(), + None => return Ok(false), + }; + + match ctx.data().repository().await?.staff_roles_contains(&roles).await { + Ok(true) => Ok(true), + Ok(false) => Err(AppError::StaffOnly { command_name: ctx.command().qualified_name.clone() }), + Err(err) => Err(AppError::from(err)), + } +} diff --git a/cipher_discord_bot/src/commands/profile.rs b/cipher_discord_bot/src/commands/profile/codes.rs similarity index 64% rename from cipher_discord_bot/src/commands/profile.rs rename to cipher_discord_bot/src/commands/profile/codes.rs index d3d52d7..d0fc485 100644 --- a/cipher_discord_bot/src/commands/profile.rs +++ b/cipher_discord_bot/src/commands/profile/codes.rs @@ -1,4 +1,3 @@ -use cipher_core::repository::staff_role_repository::StaffRoleRepository; use cipher_core::repository::user_repository::NewUser; use cipher_core::repository::user_repository::User; use cipher_core::repository::user_repository::UserRepository; @@ -11,28 +10,25 @@ use serenity::all::CreateEmbed; use serenity::all::Member; use crate::app::AppContext; -use crate::app::AppData; use crate::app::AppError; -use crate::utils; -/// Edit and show profiles. +/// Manage friend codes. #[poise::command( slash_command, subcommands( "edit", "overwrite", - "show", ), )] -pub async fn profile( +pub async fn codes( _ctx: AppContext<'_, R, R::BackendError>, ) -> Result<(), AppError> { Ok(()) } #[derive(Debug, poise::Modal)] -#[name = "Edit Profile Profile"] -struct EditProfileModal { +#[name = "Edit Friend Codes"] +struct EditCodesModal { #[name = "Pokémon Go Friend Code"] #[placeholder = "0000 0000 0000"] pokemon_go_code: Option, @@ -44,180 +40,6 @@ struct EditProfileModal { switch_code: Option, } -/// Edit your profile. -#[poise::command(slash_command)] -pub async fn edit(ctx: AppContext<'_, R, R::BackendError>) -> Result<(), AppError> { - let author_id = ctx.author().id.get(); - - let embed = match edit_user(ctx, author_id).await { - Ok(()) => CreateEmbed::new() - .title("Changes Saved") - .description("Your changes have been saved successfully.") - .color(utils::bot_color(&ctx).await), - Err(EditError::ValidationError(errors)) => CreateEmbed::new() - .title("Validation Error") - .description(errors.join("\n")) - .color(Color::RED), - Err(EditError::SerenityError(err)) => return Err(AppError::from(err)), - Err(EditError::RepositoryError(err)) => return Err(AppError::from(err)), - }; - - let reply = CreateReply::default() - .embed(embed) - .ephemeral(true); - - ctx.send(reply).await?; - - Ok(()) -} - -async fn is_staff(ctx: poise::Context<'_, AppData, AppError>) -> Result> -where - R: RepositoryProvider, -{ - let roles: Vec<_> = match ctx.author_member().await { - Some(member) => member.roles.iter().map(|r| r.get()).collect(), - None => return Ok(false), - }; - - match ctx.data().repository().await?.staff_roles_contains(&roles).await { - Ok(true) => Ok(true), - Ok(false) => Err(AppError::StaffOnly { command_name: ctx.command().qualified_name.clone() }), - Err(err) => Err(AppError::from(err)), - } -} - -/// Edit any user's profile. -#[poise::command( - slash_command, - hide_in_help, - check = "is_staff", -)] -pub async fn overwrite( - ctx: AppContext<'_, R, R::BackendError>, - #[description = "The profile to edit."] - member: Member, -) -> Result<(), AppError> { - let member_id = member.user.id.get(); - - let embed = match edit_user(ctx, member_id).await { - Ok(()) => CreateEmbed::new() - .title("Changes Saved") - .description("Your changes have been saved successfully.") - .color(utils::bot_color(&ctx).await), - Err(EditError::ValidationError(errors)) => CreateEmbed::new() - .title("Validation Error") - .description(errors.join("\n")) - .color(Color::RED), - Err(EditError::SerenityError(err)) => return Err(AppError::from(err)), - Err(EditError::RepositoryError(err)) => return Err(AppError::from(err)), - }; - - let reply = CreateReply::default() - .embed(embed) - .ephemeral(true); - - ctx.send(reply).await?; - - Ok(()) -} - -#[derive(Debug, thiserror::Error)] -enum EditError { - #[error(transparent)] - SerenityError(#[from] serenity::Error), - #[error(transparent)] - RepositoryError(#[from] RepositoryError), - #[error("Validation error")] - ValidationError(Vec), -} - -async fn edit_user( - ctx: AppContext<'_, R, R::BackendError>, - discord_user_id: u64, -) -> Result<(), EditError> -where - R: RepositoryProvider + Send + Sync, -{ - let mut repo = ctx.data.repository().await?; - - if let Some(mut user) = repo.user_by_discord_user_id(discord_user_id).await? { - let defaults = EditProfileModal { - pokemon_go_code: user.pokemon_go_code.clone(), - pokemon_pocket_code: user.pokemon_pocket_code.clone(), - switch_code: user.switch_code.clone(), - }; - - let mut data = match EditProfileModal::execute_with_defaults(ctx, defaults).await? { - Some(data) => data, - None => return Ok(()), - }; - - data.validate().map_err(EditError::ValidationError)?; - - user = User { - id: user.id, - discord_user_id: user.discord_user_id, - pokemon_go_code: data.pokemon_go_code, - pokemon_pocket_code: data.pokemon_pocket_code, - switch_code: data.switch_code, - }; - - repo.update_user(user).await?; - } else { - let mut data = match EditProfileModal::execute(ctx).await? { - Some(data) => data, - None => return Ok(()), - }; - - data.validate().map_err(EditError::ValidationError)?; - - let new_user = NewUser { - discord_user_id, - pokemon_go_code: data.pokemon_go_code, - pokemon_pocket_code: data.pokemon_pocket_code, - switch_code: data.switch_code, - }; - - repo.insert_user(new_user).await?; - } - - Ok(()) -} - -impl EditProfileModal { - fn validate(&mut self) -> Result<(), Vec> { - let mut errors = Vec::new(); - - if let Some(code) = &self.pokemon_go_code { - match parse_pokemon_go_code(code) { - Some(parsed) => self.pokemon_go_code = Some(parsed), - None => errors.push(format!("`{}` is not a valid Pokémon Go friend code.", code)), - }; - } - - if let Some(code) = &self.pokemon_pocket_code { - match parse_pokemon_pocket_code(code) { - Some(parsed) => self.pokemon_pocket_code = Some(parsed), - None => errors.push(format!("`{}` is not a valid Pokémon TCG Pocket friend code.", code)), - }; - } - - if let Some(code) = &self.switch_code { - match parse_switch_code(code) { - Some(parsed) => self.switch_code = Some(parsed), - None => errors.push(format!("`{}` is not a valid switch friend code.", code)), - }; - } - - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } - } -} - fn parse_pokemon_go_code(code: &str) -> Option { let mut chars = code.chars().peekable(); @@ -301,75 +123,155 @@ fn parse_switch_code(code: &str) -> Option { chars.next().is_none().then_some(parsed) } -/// Show your profile or someone else's. -#[poise::command(slash_command)] -pub async fn show( +impl EditCodesModal { + fn validate(&mut self) -> Result<(), Vec> { + let mut errors = Vec::new(); + + if let Some(code) = &self.pokemon_go_code { + match parse_pokemon_go_code(code) { + Some(parsed) => self.pokemon_go_code = Some(parsed), + None => errors.push(format!("`{}` is not a valid Pokémon Go friend code.", code)), + }; + } + + if let Some(code) = &self.pokemon_pocket_code { + match parse_pokemon_pocket_code(code) { + Some(parsed) => self.pokemon_pocket_code = Some(parsed), + None => errors.push(format!("`{}` is not a valid Pokémon TCG Pocket friend code.", code)), + }; + } + + if let Some(code) = &self.switch_code { + match parse_switch_code(code) { + Some(parsed) => self.switch_code = Some(parsed), + None => errors.push(format!("`{}` is not a valid switch friend code.", code)), + }; + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +#[derive(Debug, thiserror::Error)] +enum EditError { + #[error(transparent)] + SerenityError(#[from] serenity::Error), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), + #[error("Validation error")] + ValidationError(Vec), +} + +async fn execute_edit_codes_modal( ctx: AppContext<'_, R, R::BackendError>, - #[rename = "member"] - #[description = "The profile to show."] - option_member: Option, -) -> Result<(), AppError> { + discord_user_id: u64, +) -> Result<(), EditError> +where + R: RepositoryProvider + Send + Sync, +{ let mut repo = ctx.data.repository().await?; - let member = match option_member { - Some(member) => member, - None => match ctx.author_member().await { - Some(member) => member.into_owned(), - None => { - let embed = CreateEmbed::new() - .title("Error") - .description("This command can only be used in server.") - .color(Color::RED); + if let Some(mut user) = repo.user_by_discord_user_id(discord_user_id).await? { + let defaults = EditCodesModal { + pokemon_go_code: user.pokemon_go_code.clone(), + pokemon_pocket_code: user.pokemon_pocket_code.clone(), + switch_code: user.switch_code.clone(), + }; - let reply = CreateReply::default() - .embed(embed) - .ephemeral(true); + let mut data = match EditCodesModal::execute_with_defaults(ctx, defaults).await? { + Some(data) => data, + None => return Ok(()), + }; - ctx.send(reply).await?; + data.validate().map_err(EditError::ValidationError)?; - return Ok(()) - }, - }, - }; + user = User { + id: user.id, + discord_user_id: user.discord_user_id, + pokemon_go_code: data.pokemon_go_code, + pokemon_pocket_code: data.pokemon_pocket_code, + switch_code: data.switch_code, + }; - let avatar_url = member.avatar_url() - .or_else(|| member.user.avatar_url()) - .or_else(|| member.user.static_avatar_url()) - .unwrap_or_else(|| member.user.default_avatar_url()); + repo.update_user(user).await?; + } else { + let mut data = match EditCodesModal::execute(ctx).await? { + Some(data) => data, + None => return Ok(()), + }; - let embed_color = match member.colour(ctx) { - Some(color) => color, - None => utils::bot_color(&ctx).await, - }; + data.validate().map_err(EditError::ValidationError)?; - let mut embed = CreateEmbed::new() - .title(member.display_name()) - .thumbnail(avatar_url) - .color(embed_color); + let new_user = NewUser { + discord_user_id, + pokemon_go_code: data.pokemon_go_code, + pokemon_pocket_code: data.pokemon_pocket_code, + switch_code: data.switch_code, + }; - let mut is_profile_empty = true; - - if let Some(user_info) = repo.user_by_discord_user_id(member.user.id.get()).await? { - if let Some(code) = user_info.pokemon_go_code { - embed = embed.field("Pokémon Go Friend Code", code, false); - is_profile_empty = false; - } - - if let Some(code) = user_info.pokemon_pocket_code { - embed = embed.field("Pokémon TCG Pocket Friend Code", code, false); - is_profile_empty = false; - } - - if let Some(code) = user_info.switch_code { - embed = embed.field("Nintendo Switch Friend Code", code, false); - is_profile_empty = false; - } - }; - - if is_profile_empty { - embed = embed.description("No information to show."); + repo.insert_user(new_user).await?; } + Ok(()) +} + +/// Edit your friend codes. +#[poise::command(slash_command)] +async fn edit(ctx: AppContext<'_, R, R::BackendError>) -> Result<(), AppError> { + let author_id = ctx.author().id.get(); + + let embed = match execute_edit_codes_modal(ctx, author_id).await { + Ok(()) => CreateEmbed::new() + .title("Changes Saved") + .description("Your changes have been saved successfully.") + .color(crate::utils::bot_color(&ctx).await), + Err(EditError::ValidationError(errors)) => CreateEmbed::new() + .title("Validation Error") + .description(errors.join("\n")) + .color(Color::RED), + Err(EditError::SerenityError(err)) => return Err(AppError::from(err)), + Err(EditError::RepositoryError(err)) => return Err(AppError::from(err)), + }; + + let reply = CreateReply::default() + .embed(embed) + .ephemeral(true); + + ctx.send(reply).await?; + + Ok(()) +} + +/// Edit any user's friend codes. +#[poise::command( + slash_command, + hide_in_help, + check = "crate::checks::is_staff", +)] +async fn overwrite( + ctx: AppContext<'_, R, R::BackendError>, + #[description = "The profile to edit."] + member: Member, +) -> Result<(), AppError> { + let member_id = member.user.id.get(); + + let embed = match execute_edit_codes_modal(ctx, member_id).await { + Ok(()) => CreateEmbed::new() + .title("Changes Saved") + .description("Your changes have been saved successfully.") + .color(crate::utils::bot_color(&ctx).await), + Err(EditError::ValidationError(errors)) => CreateEmbed::new() + .title("Validation Error") + .description(errors.join("\n")) + .color(Color::RED), + Err(EditError::SerenityError(err)) => return Err(AppError::from(err)), + Err(EditError::RepositoryError(err)) => return Err(AppError::from(err)), + }; + let reply = CreateReply::default() .embed(embed) .ephemeral(true); diff --git a/cipher_discord_bot/src/commands/profile/mod.rs b/cipher_discord_bot/src/commands/profile/mod.rs new file mode 100644 index 0000000..cf2ecd1 --- /dev/null +++ b/cipher_discord_bot/src/commands/profile/mod.rs @@ -0,0 +1,103 @@ +use cipher_core::repository::user_repository::UserRepository; +use cipher_core::repository::RepositoryProvider; +use poise::CreateReply; +use serenity::all::Color; +use serenity::all::CreateEmbed; + +use crate::app::AppContext; +use crate::app::AppError; +use crate::utils; + +mod codes; + +/// Edit and show profiles. +#[poise::command( + slash_command, + subcommands( + "codes::codes", + "show", + ), +)] +pub async fn profile( + _ctx: AppContext<'_, R, R::BackendError>, +) -> Result<(), AppError> { + Ok(()) +} + +/// Show your profile or someone else's. +#[poise::command(slash_command)] +async fn show( + ctx: AppContext<'_, R, R::BackendError>, + #[rename = "member"] + #[description = "The profile to show."] + option_member: Option, +) -> Result<(), AppError> { + let mut repo = ctx.data.repository().await?; + + let member = match option_member { + Some(member) => member, + None => match ctx.author_member().await { + Some(member) => member.into_owned(), + None => { + let embed = CreateEmbed::new() + .title("Error") + .description("This command can only be used in server.") + .color(Color::RED); + + let reply = CreateReply::default() + .embed(embed) + .ephemeral(true); + + ctx.send(reply).await?; + + return Ok(()) + }, + }, + }; + + let avatar_url = member.avatar_url() + .or_else(|| member.user.avatar_url()) + .or_else(|| member.user.static_avatar_url()) + .unwrap_or_else(|| member.user.default_avatar_url()); + + let embed_color = match member.colour(ctx) { + Some(color) => color, + None => utils::bot_color(&ctx).await, + }; + + let mut embed = CreateEmbed::new() + .title(member.display_name()) + .thumbnail(avatar_url) + .color(embed_color); + + let mut is_profile_empty = true; + + if let Some(user_info) = repo.user_by_discord_user_id(member.user.id.get()).await? { + if let Some(code) = user_info.pokemon_go_code { + embed = embed.field("Pokémon Go Friend Code", code, false); + is_profile_empty = false; + } + + if let Some(code) = user_info.pokemon_pocket_code { + embed = embed.field("Pokémon TCG Pocket Friend Code", code, false); + is_profile_empty = false; + } + + if let Some(code) = user_info.switch_code { + embed = embed.field("Nintendo Switch Friend Code", code, false); + is_profile_empty = false; + } + }; + + if is_profile_empty { + embed = embed.description("No information to show."); + } + + let reply = CreateReply::default() + .embed(embed) + .ephemeral(true); + + ctx.send(reply).await?; + + Ok(()) +} diff --git a/cipher_discord_bot/src/main.rs b/cipher_discord_bot/src/main.rs index 143094b..673959c 100644 --- a/cipher_discord_bot/src/main.rs +++ b/cipher_discord_bot/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod checks; mod cli; mod commands; mod utils;