From 417621d422ef134b4baca726098989c2c96eac2a Mon Sep 17 00:00:00 2001 From: Kappeh Date: Thu, 30 Jan 2025 02:27:13 +0000 Subject: [PATCH] Add commands for viewing and editing user profiles --- cipher_core/src/repository/mod.rs | 2 +- cipher_discord_bot/src/commands/mod.rs | 2 + cipher_discord_bot/src/commands/profile.rs | 295 +++++++++++++++++++++ 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 cipher_discord_bot/src/commands/profile.rs diff --git a/cipher_core/src/repository/mod.rs b/cipher_core/src/repository/mod.rs index 016f2e7..341d521 100644 --- a/cipher_core/src/repository/mod.rs +++ b/cipher_core/src/repository/mod.rs @@ -6,7 +6,7 @@ pub mod user_repository; #[async_trait::async_trait] pub trait RepositoryProvider { - type BackendError: std::error::Error; + type BackendError: std::error::Error + Send + Sync; type Repository<'a>: Repository::BackendError> + Send + Sync where diff --git a/cipher_discord_bot/src/commands/mod.rs b/cipher_discord_bot/src/commands/mod.rs index 5256dbf..f1682be 100644 --- a/cipher_discord_bot/src/commands/mod.rs +++ b/cipher_discord_bot/src/commands/mod.rs @@ -3,6 +3,7 @@ use cipher_core::repository::RepositoryProvider; use crate::app::AppCommand; mod ping; +mod profile; pub fn commands() -> Vec> where @@ -10,5 +11,6 @@ where { vec![ ping::ping(), + profile::profile(), ] } diff --git a/cipher_discord_bot/src/commands/profile.rs b/cipher_discord_bot/src/commands/profile.rs new file mode 100644 index 0000000..753ee7e --- /dev/null +++ b/cipher_discord_bot/src/commands/profile.rs @@ -0,0 +1,295 @@ +use cipher_core::repository::user_repository::NewUser; +use cipher_core::repository::user_repository::UserRepository; +use cipher_core::repository::RepositoryProvider; +use poise::CreateReply; +use poise::Modal; +use serenity::all::Color; +use serenity::all::CreateEmbed; + +use crate::app::AppContext; +use crate::app::AppError; +use crate::utils; + +#[poise::command( + slash_command, + subcommands( + "edit", + "show", + ), +)] +pub async fn profile( + _ctx: AppContext<'_, R, R::BackendError>, +) -> Result<(), AppError> { + Ok(()) +} + +#[derive(Debug, poise::Modal)] +#[name = "Edit Your Profile"] +struct EditProfileModal { + #[name = "Pokémon Go Friend Code"] + #[placeholder = "0000 0000 0000"] + pokemon_go_code: Option, + #[name = "Pokémon TCG Pocket Friend Code"] + #[placeholder = "0000 0000 0000 0000"] + pokemon_pocket_code: Option, + #[name = "Nintendo Switch Friend Code"] + #[placeholder = "SW-0000-0000-0000"] + switch_code: Option, +} + +#[poise::command(slash_command)] +pub async fn edit(ctx: AppContext<'_, R, R::BackendError>) -> Result<(), AppError> { + let mut repo = ctx.data.repository().await?; + let author_id = ctx.author().id.get(); + + let errors; + + if let Some(mut user) = repo.user_by_discord_user_id(author_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(()), + }; + + errors = data.validate(); + + if errors.is_empty() { + user.pokemon_go_code = data.pokemon_go_code; + user.pokemon_pocket_code = data.pokemon_pocket_code; + user.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(()), + }; + + errors = data.validate(); + + if errors.is_empty() { + let new_user = NewUser { + discord_user_id: author_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?; + } + } + + let embed = if errors.is_empty() { + CreateEmbed::new() + .title("Changes Saved") + .description("Your changes have been saved successfully.") + .color(utils::bot_color(&ctx).await) + } else { + CreateEmbed::new() + .title("Validation Error") + .description(errors.join("\n")) + .color(Color::RED) + }; + + let reply = CreateReply::default() + .embed(embed) + .ephemeral(true); + + ctx.send(reply).await?; + + Ok(()) +} + +impl EditProfileModal { + fn validate(&mut self) -> 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)), + }; + } + + return errors; + } +} + +fn parse_pokemon_go_code(code: &str) -> Option { + let mut chars = code.chars().peekable(); + + let mut parsed = String::new(); + + for i in 0..3 { + if i > 0 { + let c = *chars.peek()?; + if c == '-' || c == ' ' { + chars.next(); + } + parsed.push(' '); + } + + for _ in 0..4 { + let c = chars.next()?; + if !c.is_numeric() { + return None; + } + parsed.push(c); + } + } + + chars.next().is_none().then_some(parsed) +} + +fn parse_pokemon_pocket_code(code: &str) -> Option { + let mut chars = code.chars().peekable(); + + let mut parsed = String::new(); + + for i in 0..4 { + if i > 0 { + let c = *chars.peek()?; + if c == '-' || c == ' ' { + chars.next(); + } + parsed.push(' '); + } + + for _ in 0..4 { + let c = chars.next()?; + if !c.is_numeric() { + return None; + } + parsed.push(c); + } + } + + chars.next().is_none().then_some(parsed) +} + +fn parse_switch_code(code: &str) -> Option { + let mut chars = code.chars().map(|c| c.to_ascii_uppercase()).peekable(); + + let mut parsed = String::from("SW"); + + if *chars.peek()? == 'S' { + chars.next(); + if chars.next()? != 'W' { + return None; + } + } + + for _ in 0..3 { + let c = *chars.peek()?; + if c == '-' || c == ' ' { + chars.next(); + } + parsed.push('-'); + + for _ in 0..4 { + let c = chars.next()?; + if !c.is_numeric() { + return None; + } + parsed.push(c); + } + } + + chars.next().is_none().then_some(parsed) +} + +#[poise::command(slash_command)] +pub async fn show( + ctx: AppContext<'_, R, R::BackendError>, + #[rename = "member"] + 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(()) +}