feat(discord): Added /avatar command
- Added formattable error strings
This commit is contained in:
parent
2640821a05
commit
1db7ce877e
5 changed files with 236 additions and 13 deletions
|
|
@ -1,3 +1,4 @@
|
|||
use poise::{serenity_prelude::{Attachment, CreateAttachment, CreateMessage}, CreateReply};
|
||||
use squad_quest::{SquadObject, map::Map};
|
||||
|
||||
use crate::{Context, account::fetch_or_init_account, error::Error, commands::guild};
|
||||
|
|
@ -96,3 +97,99 @@ pub async fn r#move(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change avatar on web map
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
guild_only,
|
||||
check = "guild",
|
||||
name_localized("ru", "аватар"),
|
||||
description_localized("ru", "Сменить аватар на веб карте"),
|
||||
)]
|
||||
pub async fn avatar(
|
||||
ctx: Context<'_>,
|
||||
#[description = "URL to the avatar"]
|
||||
#[name_localized("ru", "ссылка")]
|
||||
#[description_localized("ru", "Ссылка на аватар")]
|
||||
url: Option<String>,
|
||||
#[description = "Attachment to use as avatar"]
|
||||
#[name_localized("ru", "вложение")]
|
||||
#[description_localized("ru", "Вложение, используемое как аватар")]
|
||||
attachment: Option<Attachment>,
|
||||
) -> Result<(), Error> {
|
||||
let user_id = ctx.author().id.to_string();
|
||||
let mut accounts = ctx.data().config.load_accounts();
|
||||
let Some(account) = accounts.iter_mut().find(|a| a.id == user_id) else {
|
||||
return Err(Error::AccountNotFound);
|
||||
};
|
||||
|
||||
if url.is_none() && attachment.is_none() {
|
||||
return Err(Error::NoUrlOrAttachment);
|
||||
}
|
||||
|
||||
if url.is_some() && attachment.is_some() {
|
||||
return Err(Error::BothUrlAndAttachment);
|
||||
}
|
||||
|
||||
let strings = &ctx.data().strings;
|
||||
let formatter = strings.formatter();
|
||||
|
||||
if let Some(url) = url {
|
||||
let attachment = CreateAttachment::url(ctx, &url).await?;
|
||||
let reply_string = formatter.fmt(&strings.map.processing_url);
|
||||
let builder = CreateMessage::new()
|
||||
.content(reply_string)
|
||||
.add_file(attachment.clone());
|
||||
let message = ctx.channel_id().send_message(ctx, builder).await?;
|
||||
|
||||
let attachment_check = message.attachments.first().expect("we just sent it");
|
||||
if attachment_check.width.is_none()
|
||||
|| !attachment_check.content_type
|
||||
.as_ref()
|
||||
.is_some_and(|t| t.starts_with("image/")) {
|
||||
message.delete(ctx).await?;
|
||||
return Err(Error::NonImageAttachment);
|
||||
}
|
||||
let data = account.data.as_mut().expect("automatically created");
|
||||
data.insert("avatar".to_string(), url);
|
||||
|
||||
message.delete(ctx).await?;
|
||||
|
||||
let reply_string = formatter.fmt(&strings.map.updated_avatar);
|
||||
let builder = CreateReply::default()
|
||||
.content(reply_string)
|
||||
.attachment(attachment)
|
||||
.reply(true);
|
||||
ctx.send(builder).await?;
|
||||
|
||||
} else if let Some(attachment) = attachment {
|
||||
if attachment.width.is_none()
|
||||
|| !attachment.content_type
|
||||
.as_ref()
|
||||
.is_some_and(|t| t.starts_with("image/")) {
|
||||
return Err(Error::NonImageAttachment);
|
||||
}
|
||||
|
||||
let reply_string = formatter.fmt(&strings.map.updated_avatar);
|
||||
let copied_attachment = CreateAttachment::url(ctx, &attachment.url).await?;
|
||||
|
||||
let data = account.data.as_mut().expect("automatically created");
|
||||
data.insert("avatar".to_string(), attachment.url);
|
||||
|
||||
let path = ctx.data().config.full_accounts_path();
|
||||
account.save(path)?;
|
||||
|
||||
let builder = CreateReply::default()
|
||||
.content(reply_string)
|
||||
.attachment(copied_attachment)
|
||||
.reply(true);
|
||||
|
||||
ctx.send(builder).await?;
|
||||
}
|
||||
|
||||
let path = ctx.data().config.full_accounts_path();
|
||||
account.save(path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use std::error::Error as StdError;
|
||||
use poise::{CreateReply, serenity_prelude::GuildId};
|
||||
use squad_quest::quest::Quest;
|
||||
|
||||
use crate::{Context, Data, Error};
|
||||
use crate::{error, Context, Data, Error};
|
||||
|
||||
pub mod quest;
|
||||
pub mod init;
|
||||
|
|
@ -49,14 +50,42 @@ pub async fn error_handler(error: poise::FrameworkError<'_, Data, Error>) {
|
|||
print_error_recursively(&error);
|
||||
if let Some(ctx) = error.ctx() {
|
||||
let user = ctx.author().display_name();
|
||||
eprintln!("User: {user} ({id})", id = ctx.author().id);
|
||||
|
||||
let response = match error {
|
||||
poise::FrameworkError::Command { error, .. } => {
|
||||
eprintln!("Invokation string: {}", ctx.invocation_string());
|
||||
format!("Internal server error: {error}")
|
||||
},
|
||||
_ => format!("Internal server error: {error}"),
|
||||
eprintln!("User: {user} ({id})", id = ctx.author().id);
|
||||
eprintln!("Invokation string: {}", ctx.invocation_string());
|
||||
|
||||
let strings = &ctx.data().strings;
|
||||
|
||||
let response = if let poise::FrameworkError::Command { error, .. } = error {
|
||||
let formatter = match error {
|
||||
error::Error::QuestNotFound(id) |
|
||||
error::Error::QuestIsPublic(id) |
|
||||
error::Error::QuestIsCompleted(id) |
|
||||
error::Error::QuestLimitExceeded(id) =>
|
||||
strings.formatter().quest(&Quest { id, ..Default::default() }),
|
||||
|
||||
error::Error::InsufficientFunds(amount) =>
|
||||
strings.formatter().value(amount),
|
||||
|
||||
error::Error::RoomNotFound(value) |
|
||||
error::Error::RoomAlreadyUnlocked(value) |
|
||||
error::Error::CannotReach(value) =>
|
||||
strings.formatter().value(value),
|
||||
|
||||
error::Error::SerenityError(ref error) =>
|
||||
strings.formatter().text(error),
|
||||
|
||||
error::Error::SquadQuestError(ref error) =>
|
||||
strings.formatter().text(error),
|
||||
|
||||
_ => strings.formatter(),
|
||||
};
|
||||
|
||||
let error_string = error.formattable_string(&strings.error);
|
||||
formatter.fmt(error_string)
|
||||
} else {
|
||||
let formatter = strings.formatter().text(&error);
|
||||
formatter.fmt(&strings.error.non_command_error)
|
||||
};
|
||||
|
||||
if let Err(error) = ctx.send(CreateReply::default().content(response).ephemeral(true)).await {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ use std::fmt::Display;
|
|||
use poise::serenity_prelude as serenity;
|
||||
use squad_quest::error::MapError;
|
||||
|
||||
use crate::strings::ErrorStrings;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
|
|
@ -23,6 +25,9 @@ pub enum Error {
|
|||
TimerSet,
|
||||
NotThisGuild,
|
||||
QuestLimitExceeded(u16),
|
||||
BothUrlAndAttachment,
|
||||
NoUrlOrAttachment,
|
||||
NonImageAttachment,
|
||||
}
|
||||
|
||||
impl From<serenity::Error> for Error {
|
||||
|
|
@ -55,9 +60,9 @@ impl Display for Error {
|
|||
Self::QuestNotFound(u16) => write!(f, "quest #{u16} not found"),
|
||||
Self::QuestIsPublic(u16) => write!(f, "quest #{u16} is already public"),
|
||||
Self::QuestIsCompleted(u16) => write!(f, "quest #{u16} is already completed for this user"),
|
||||
Self::NoContent => write!(f, "no text or attachment was specified"),
|
||||
Self::NoChannelOrUser => write!(f, "no channel or user was specified"),
|
||||
Self::BothChannelAndUser => write!(f, "both channel and user was specified"),
|
||||
Self::NoContent => write!(f, "no text or attachment were specified"),
|
||||
Self::NoChannelOrUser => write!(f, "no channel or user were specified"),
|
||||
Self::BothChannelAndUser => write!(f, "both channel and user were specified"),
|
||||
Self::SerenityError(_) => write!(f, "discord interaction error"),
|
||||
Self::SquadQuestError(_) => write!(f, "internal logic error"),
|
||||
Self::AccountNotFound => write!(f, "account not found"),
|
||||
|
|
@ -69,6 +74,9 @@ impl Display for Error {
|
|||
Self::TimerSet => write!(f, "timer is already set"),
|
||||
Self::NotThisGuild => write!(f, "cannot be used in this guild"),
|
||||
Self::QuestLimitExceeded(id) => write!(f, "exceeded limit for quest #{id}"),
|
||||
Self::BothUrlAndAttachment => write!(f, "both url and attachment were specified"),
|
||||
Self::NoUrlOrAttachment => write!(f, "no url or attachment were specified"),
|
||||
Self::NonImageAttachment => write!(f, "attachment is not an image"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -90,11 +98,39 @@ impl std::error::Error for Error {
|
|||
Self::CannotReach(_) |
|
||||
Self::TimerSet |
|
||||
Self::NotThisGuild |
|
||||
Self::QuestLimitExceeded(_) => None,
|
||||
Self::QuestLimitExceeded(_) |
|
||||
Self::BothUrlAndAttachment |
|
||||
Self::NoUrlOrAttachment |
|
||||
Self::NonImageAttachment => None,
|
||||
Self::SerenityError(error) => Some(error),
|
||||
Self::SquadQuestError(error) => Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<'a> Error {
|
||||
pub fn formattable_string(&self, errors: &'a ErrorStrings) -> &'a str {
|
||||
match self {
|
||||
Self::QuestNotFound(_) => &errors.quest_not_found,
|
||||
Self::QuestIsPublic(_) => &errors.quest_is_public,
|
||||
Self::QuestIsCompleted(_) => &errors.quest_is_completed,
|
||||
Self::NoContent => &errors.no_content,
|
||||
Self::NoChannelOrUser => &errors.no_channel_or_user,
|
||||
Self::BothChannelAndUser => &errors.both_channel_and_user,
|
||||
Self::SerenityError(_) => &errors.discord_error,
|
||||
Self::SquadQuestError(_) => &errors.library_error,
|
||||
Self::AccountNotFound => &errors.account_not_found,
|
||||
Self::AccountIsSelf => &errors.account_is_self,
|
||||
Self::InsufficientFunds(_) => &errors.insufficient_funds,
|
||||
Self::RoomNotFound(_) => &errors.room_not_found,
|
||||
Self::RoomAlreadyUnlocked(_) => &errors.room_already_unlocked,
|
||||
Self::CannotReach(_) => &errors.cannot_reach,
|
||||
Self::TimerSet => &errors.timer_set,
|
||||
Self::NotThisGuild => &errors.not_this_guild,
|
||||
Self::QuestLimitExceeded(_) => &errors.quest_limit_exceeded,
|
||||
Self::BothUrlAndAttachment => &errors.both_url_and_attachment,
|
||||
Self::NoUrlOrAttachment => &errors.no_url_or_attachment,
|
||||
Self::NonImageAttachment => &errors.non_image_attachment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ async fn main() {
|
|||
commands::account::reset(),
|
||||
commands::map::unlock(),
|
||||
commands::map::r#move(),
|
||||
commands::map::avatar(),
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ pub struct Strings {
|
|||
pub scoreboard: Scoreboard,
|
||||
pub social: Social,
|
||||
pub quest: QuestStrings,
|
||||
pub error: ErrorStrings,
|
||||
}
|
||||
|
||||
impl Default for Strings {
|
||||
|
|
@ -160,6 +161,7 @@ impl Default for Strings {
|
|||
social: Social::default(),
|
||||
account: AccountReplies::default(),
|
||||
map: MapReplies::default(),
|
||||
error: ErrorStrings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -391,6 +393,8 @@ impl Default for AccountReplies {
|
|||
pub struct MapReplies {
|
||||
pub room_unlocked: String,
|
||||
pub moved_to_room: String,
|
||||
pub updated_avatar: String,
|
||||
pub processing_url: String,
|
||||
}
|
||||
|
||||
impl Default for MapReplies {
|
||||
|
|
@ -398,6 +402,62 @@ impl Default for MapReplies {
|
|||
Self {
|
||||
room_unlocked: "Unlocked room #{value}. Your balance: {b.current}".to_string(),
|
||||
moved_to_room: "Moved to room #{value}".to_string(),
|
||||
updated_avatar: "Successfully changed avatar".to_string(),
|
||||
processing_url: "Processing URL...".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct ErrorStrings {
|
||||
pub non_command_error: String,
|
||||
pub quest_not_found: String,
|
||||
pub quest_is_public: String,
|
||||
pub quest_is_completed: String,
|
||||
pub no_content: String,
|
||||
pub no_channel_or_user: String,
|
||||
pub both_channel_and_user: String,
|
||||
pub discord_error: String,
|
||||
pub library_error: String,
|
||||
pub account_not_found: String,
|
||||
pub account_is_self: String,
|
||||
pub insufficient_funds: String,
|
||||
pub room_not_found: String,
|
||||
pub room_already_unlocked: String,
|
||||
pub cannot_reach: String,
|
||||
pub timer_set: String,
|
||||
pub not_this_guild: String,
|
||||
pub quest_limit_exceeded: String,
|
||||
pub both_url_and_attachment: String,
|
||||
pub no_url_or_attachment: String,
|
||||
pub non_image_attachment: String,
|
||||
}
|
||||
|
||||
impl Default for ErrorStrings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
non_command_error: "Internal server error: {text}".to_string(),
|
||||
quest_not_found: "Quest {q.id} not found".to_string(),
|
||||
quest_is_public: "Quest {q.id} is already public".to_string(),
|
||||
quest_is_completed: "Quest {q.id} is already completed for this user".to_string(),
|
||||
no_content: "No text or attachment were specified".to_string(),
|
||||
no_channel_or_user: "No channel or user were specified".to_string(),
|
||||
both_channel_and_user: "Both channel and user were specified".to_string(),
|
||||
discord_error: "Discord interaction error: {text}".to_string(),
|
||||
library_error: "Some internal logic error: {text}".to_string(),
|
||||
account_not_found: "Given account was not found".to_string(),
|
||||
account_is_self: "Given account is the same as command invoker".to_string(),
|
||||
insufficient_funds: "You don't have {value} points".to_string(),
|
||||
room_not_found: "Room #{value} not found".to_string(),
|
||||
room_already_unlocked: "Room #{value} is already unlocked for this account".to_string(),
|
||||
cannot_reach: "You cannot reach room #{value}".to_string(),
|
||||
timer_set: "Timer is already set".to_string(),
|
||||
not_this_guild: "Bot cannot be used in this guild".to_string(),
|
||||
quest_limit_exceeded: "Exceeded limit for quest {q.id}".to_string(),
|
||||
both_url_and_attachment: "Both URL and attachment were specified".to_string(),
|
||||
no_url_or_attachment: "No URL or attachment were specified".to_string(),
|
||||
non_image_attachment: "Given attachment is not an image".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue