Move friend code management into seperate command

This commit is contained in:
2025-02-02 19:07:13 +00:00
parent db62c61e14
commit 4bb22bc8a4
4 changed files with 266 additions and 239 deletions

View File

@@ -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<R>(ctx: poise::Context<'_, AppData<R>, AppError<R::BackendError>>) -> Result<bool, AppError<R::BackendError>>
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)),
}
}

View File

@@ -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::NewUser;
use cipher_core::repository::user_repository::User; use cipher_core::repository::user_repository::User;
use cipher_core::repository::user_repository::UserRepository; use cipher_core::repository::user_repository::UserRepository;
@@ -11,28 +10,25 @@ use serenity::all::CreateEmbed;
use serenity::all::Member; use serenity::all::Member;
use crate::app::AppContext; use crate::app::AppContext;
use crate::app::AppData;
use crate::app::AppError; use crate::app::AppError;
use crate::utils;
/// Edit and show profiles. /// Manage friend codes.
#[poise::command( #[poise::command(
slash_command, slash_command,
subcommands( subcommands(
"edit", "edit",
"overwrite", "overwrite",
"show",
), ),
)] )]
pub async fn profile<R: RepositoryProvider + Send + Sync>( pub async fn codes<R: RepositoryProvider + Send + Sync>(
_ctx: AppContext<'_, R, R::BackendError>, _ctx: AppContext<'_, R, R::BackendError>,
) -> Result<(), AppError<R::BackendError>> { ) -> Result<(), AppError<R::BackendError>> {
Ok(()) Ok(())
} }
#[derive(Debug, poise::Modal)] #[derive(Debug, poise::Modal)]
#[name = "Edit Profile Profile"] #[name = "Edit Friend Codes"]
struct EditProfileModal { struct EditCodesModal {
#[name = "Pokémon Go Friend Code"] #[name = "Pokémon Go Friend Code"]
#[placeholder = "0000 0000 0000"] #[placeholder = "0000 0000 0000"]
pokemon_go_code: Option<String>, pokemon_go_code: Option<String>,
@@ -44,180 +40,6 @@ struct EditProfileModal {
switch_code: Option<String>, switch_code: Option<String>,
} }
/// Edit your profile.
#[poise::command(slash_command)]
pub async fn edit<R: RepositoryProvider + Send + Sync>(ctx: AppContext<'_, R, R::BackendError>) -> Result<(), AppError<R::BackendError>> {
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<R>(ctx: poise::Context<'_, AppData<R>, AppError<R::BackendError>>) -> Result<bool, AppError<R::BackendError>>
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<R: RepositoryProvider + Send + Sync>(
ctx: AppContext<'_, R, R::BackendError>,
#[description = "The profile to edit."]
member: Member,
) -> Result<(), AppError<R::BackendError>> {
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<E> {
#[error(transparent)]
SerenityError(#[from] serenity::Error),
#[error(transparent)]
RepositoryError(#[from] RepositoryError<E>),
#[error("Validation error")]
ValidationError(Vec<String>),
}
async fn edit_user<R>(
ctx: AppContext<'_, R, R::BackendError>,
discord_user_id: u64,
) -> Result<(), EditError<R::BackendError>>
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<String>> {
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<String> { fn parse_pokemon_go_code(code: &str) -> Option<String> {
let mut chars = code.chars().peekable(); let mut chars = code.chars().peekable();
@@ -301,75 +123,155 @@ fn parse_switch_code(code: &str) -> Option<String> {
chars.next().is_none().then_some(parsed) chars.next().is_none().then_some(parsed)
} }
/// Show your profile or someone else's. impl EditCodesModal {
#[poise::command(slash_command)] fn validate(&mut self) -> Result<(), Vec<String>> {
pub async fn show<R: RepositoryProvider + Send + Sync>( 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<E> {
#[error(transparent)]
SerenityError(#[from] serenity::Error),
#[error(transparent)]
RepositoryError(#[from] RepositoryError<E>),
#[error("Validation error")]
ValidationError(Vec<String>),
}
async fn execute_edit_codes_modal<R>(
ctx: AppContext<'_, R, R::BackendError>, ctx: AppContext<'_, R, R::BackendError>,
#[rename = "member"] discord_user_id: u64,
#[description = "The profile to show."] ) -> Result<(), EditError<R::BackendError>>
option_member: Option<serenity::all::Member>, where
) -> Result<(), AppError<R::BackendError>> { R: RepositoryProvider + Send + Sync,
{
let mut repo = ctx.data.repository().await?; let mut repo = ctx.data.repository().await?;
let member = match option_member { if let Some(mut user) = repo.user_by_discord_user_id(discord_user_id).await? {
Some(member) => member, let defaults = EditCodesModal {
None => match ctx.author_member().await { pokemon_go_code: user.pokemon_go_code.clone(),
Some(member) => member.into_owned(), pokemon_pocket_code: user.pokemon_pocket_code.clone(),
None => { switch_code: user.switch_code.clone(),
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() let mut data = match EditCodesModal::execute_with_defaults(ctx, defaults).await? {
.or_else(|| member.user.avatar_url()) Some(data) => data,
.or_else(|| member.user.static_avatar_url()) None => return Ok(()),
.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() data.validate().map_err(EditError::ValidationError)?;
.title(member.display_name())
.thumbnail(avatar_url)
.color(embed_color);
let mut is_profile_empty = true; user = User {
id: user.id,
if let Some(user_info) = repo.user_by_discord_user_id(member.user.id.get()).await? { discord_user_id: user.discord_user_id,
if let Some(code) = user_info.pokemon_go_code { pokemon_go_code: data.pokemon_go_code,
embed = embed.field("Pokémon Go Friend Code", code, false); pokemon_pocket_code: data.pokemon_pocket_code,
is_profile_empty = false; switch_code: data.switch_code,
}
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 { repo.update_user(user).await?;
embed = embed.description("No information to show."); } else {
let mut data = match EditCodesModal::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(())
}
/// Edit your friend codes.
#[poise::command(slash_command)]
async fn edit<R: RepositoryProvider + Send + Sync>(ctx: AppContext<'_, R, R::BackendError>) -> Result<(), AppError<R::BackendError>> {
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<R: RepositoryProvider + Send + Sync>(
ctx: AppContext<'_, R, R::BackendError>,
#[description = "The profile to edit."]
member: Member,
) -> Result<(), AppError<R::BackendError>> {
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() let reply = CreateReply::default()
.embed(embed) .embed(embed)
.ephemeral(true); .ephemeral(true);

View File

@@ -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<R: RepositoryProvider + Send + Sync>(
_ctx: AppContext<'_, R, R::BackendError>,
) -> Result<(), AppError<R::BackendError>> {
Ok(())
}
/// Show your profile or someone else's.
#[poise::command(slash_command)]
async fn show<R: RepositoryProvider + Send + Sync>(
ctx: AppContext<'_, R, R::BackendError>,
#[rename = "member"]
#[description = "The profile to show."]
option_member: Option<serenity::all::Member>,
) -> Result<(), AppError<R::BackendError>> {
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(())
}

View File

@@ -1,4 +1,5 @@
mod app; mod app;
mod checks;
mod cli; mod cli;
mod commands; mod commands;
mod utils; mod utils;