From 787118309accdd75f62e8daf5cc158b901dd075e Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 17 Dec 2025 14:43:40 +0300 Subject: [PATCH] feat(discord): Moved most strings to Strings - Added Error::AccountIsSelf variant - /balance give to self now returns error --- discord/src/commands/account.rs | 76 ++++++++++++-------- discord/src/commands/answer.rs | 59 ++++++++-------- discord/src/commands/init.rs | 4 +- discord/src/commands/map.rs | 15 +++- discord/src/commands/social.rs | 56 +++++++++------ discord/src/error.rs | 3 + discord/src/strings.rs | 119 ++++++++++++++++++++++++++++++-- 7 files changed, 247 insertions(+), 85 deletions(-) diff --git a/discord/src/commands/account.rs b/discord/src/commands/account.rs index ebaede1..7dc5cb2 100644 --- a/discord/src/commands/account.rs +++ b/discord/src/commands/account.rs @@ -1,23 +1,24 @@ -use poise::serenity_prelude::UserId; +use poise::serenity_prelude::User; use squad_quest::{SquadObject, account::Account, map::Map}; -use crate::{Context, Error, account::{account_full_balance, account_rooms_value, account_user_id, fetch_or_init_account}}; +use crate::{Context, Error, account::{account_full_balance, account_user_id, fetch_or_init_account}, strings::StringFormatter}; -async fn account_balance_string(ctx: &Context<'_>, account: &Account, map: &Map) -> String { - let rooms_value = account_rooms_value(account, map); - let full_balance = account_full_balance(account, map); +async fn account_balance_string( + ctx: &Context<'_>, + account: &Account, + map: &Map, + mut formatter: StringFormatter +) -> String { let account_id = account_user_id(&account); - - let Ok(user) = account_id - .to_user(ctx) - .await else { + + let Ok(user) = account_id.to_user(ctx).await else { return String::new(); }; - let name = user.display_name(); - format!("\n{name}: **{full_balance}** points (**{balance}** on balance \ - + **{rooms_value}** unlocked rooms networth)", - balance = account.balance, - ) + + let strings = &ctx.data().strings; + formatter = formatter.user(&user).balance(account, map); + + formatter.fmt(&strings.scoreboard.line_format) } #[poise::command( @@ -28,11 +29,11 @@ async fn account_balance_string(ctx: &Context<'_>, account: &Account, map: &Map) )] pub async fn reset( ctx: Context<'_>, - who: UserId, + who: User, ) -> Result<(), Error> { let accounts = ctx.data().config.load_accounts(); - let acc_id = format!("{}", who.get()); + let acc_id = format!("{}", who.id.get()); if let None = accounts.iter().find(|a| a.id == acc_id) { return Err(Error::AccountNotFound); @@ -42,7 +43,10 @@ pub async fn reset( path.push(format!("{acc_id}.toml")); Account::delete(path)?; - let reply_string = "User was successfully reset.".to_string(); + let strings = &ctx.data().strings; + let formatter = strings.formatter().user(&who); + + let reply_string = formatter.fmt(&strings.account.reset); ctx.reply(reply_string).await?; Ok(()) @@ -59,6 +63,9 @@ pub async fn scoreboard( let map_path = ctx.data().config.full_map_path(); let map = Map::load(map_path).expect("map.toml should exist"); + let strings = &ctx.data().strings; + let mut formatter = strings.formatter(); + let mut accounts = ctx.data().config.load_accounts(); accounts.sort_by(|a,b| { let a_balance = account_full_balance(a, &map); @@ -68,13 +75,14 @@ pub async fn scoreboard( let this_user = ctx.author().id; - let mut reply_string = "Current scoreboard:".to_string(); + let mut reply_string = formatter.fmt(&strings.scoreboard.header); for account in accounts { let user_id = account_user_id(&account); - let mut str = account_balance_string(&ctx, &account, &map).await; + let mut str = account_balance_string(&ctx, &account, &map, formatter.clone()).await; if user_id == this_user { - str = format!("__{str}__ << You"); + formatter = formatter.text(&str); + str = formatter.fmt(&strings.scoreboard.you_format); } reply_string.push_str(&str); @@ -104,16 +112,20 @@ pub async fn balance( )] pub async fn give( ctx: Context<'_>, - who: UserId, + who: User, amount: u32, ) -> Result<(), Error> { + if ctx.author() == &who { + return Err(Error::AccountIsSelf); + } + let config = &ctx.data().config; let mut accounts = config.load_accounts(); let user_id = format!("{}", ctx.author().id.get()); let mut user_account = fetch_or_init_account(config, user_id); - let who_id = format!("{}", who.get()); + let who_id = format!("{}", who.id.get()); let Some(other_account) = accounts.iter_mut().find(|a| a.id == who_id ) else { return Err(Error::AccountNotFound); }; @@ -129,8 +141,13 @@ pub async fn give( user_account.save(accounts_path.clone())?; other_account.save(accounts_path)?; - let reply_string = format!("Given money to user.\n\ - Your new balance: {} points.", user_account.balance); + let strings = &ctx.data().strings; + let formatter = strings.formatter() + .user(&who) + .value(amount) + .current_balance(&user_account); + + let reply_string = formatter.fmt(&strings.account.give_pt); ctx.reply(reply_string).await?; Ok(()) @@ -144,12 +161,12 @@ pub async fn give( )] pub async fn set( ctx: Context<'_>, - who: UserId, + who: User, amount: u32, ) -> Result<(), Error> { let mut accounts = ctx.data().config.load_accounts(); - let who_id = format!("{}", who.get()); + let who_id = format!("{}", who.id.get()); let Some(account) = accounts.iter_mut().find(|a| a.id == who_id ) else { return Err(Error::AccountNotFound); }; @@ -157,8 +174,13 @@ pub async fn set( account.balance = amount; let accounts_path = ctx.data().config.full_accounts_path(); account.save(accounts_path)?; + + let strings = &ctx.data().strings; + let formatter = strings.formatter() + .user(&who) + .current_balance(&account); - let reply_string = format!("Set user balance to {amount}."); + let reply_string = formatter.fmt(&strings.account.set_pt); ctx.reply(reply_string).await?; Ok(()) diff --git a/discord/src/commands/answer.rs b/discord/src/commands/answer.rs index fd55666..f01b9c7 100644 --- a/discord/src/commands/answer.rs +++ b/discord/src/commands/answer.rs @@ -1,4 +1,4 @@ -use poise::serenity_prelude::{Attachment, ComponentInteractionCollector, CreateActionRow, CreateAttachment, CreateButton, CreateMessage, EditMessage, Mentionable}; +use poise::serenity_prelude::{Attachment, ComponentInteractionCollector, CreateActionRow, CreateAttachment, CreateButton, CreateMessage, EditMessage}; use squad_quest::SquadObject; use crate::{Context, Error, account::fetch_or_init_account}; @@ -33,7 +33,7 @@ pub async fn answer( .find(|q| q.id == quest_id) else { return Err(Error::QuestNotFound(quest_id)); }; - + let mut files: Vec = Vec::new(); for file in [file1, file2, file3] { if let Some(f) = file { @@ -45,23 +45,30 @@ pub async fn answer( return Err(Error::NoContent); } - let text_ans = match text { - Some(text) => format!("\n### Passed answer:\n{text}"), + let strings = &ctx.data().strings; + let mut formatter = strings.formatter() + .user(ctx.author()) + .quest(quest); + + let text_ans = match text { + Some(text) => { + formatter = formatter.text(text); + formatter.fmt(&strings.answer.text) + }, None => String::new(), }; let attachment_notice = if files.len() == 0 { String::new() } else { - "\nPassed answer has attachments.".to_string() + formatter.fmt(&strings.answer.attachment_notice) }; - let content = format!("## From: {user}\n\ - ### Quest #{quest_id}: {quest_name}\n\ - ### Expected answer:\n\ - ||{quest_answer}||{text_ans}{attachment_notice}", - user = ctx.author().mention(), - quest_name = quest.name, - quest_answer = quest.answer, - ); + let content = [ + formatter.fmt(&strings.answer.from), + formatter.fmt(&strings.answer.quest), + formatter.fmt(&strings.answer.expected), + text_ans, + attachment_notice, + ].join(""); let mut attachments: Vec = Vec::new(); @@ -91,19 +98,21 @@ pub async fn answer( let mut message = ans_channel.send_message(ctx, builder).await?; - let reply_string = "Your answer has been posted.".to_string(); + let reply_string = formatter.fmt(&strings.answer.reply.initial); ctx.reply(reply_string).await?; if let Some(press) = ComponentInteractionCollector::new(ctx) .filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string())) .await { - let admin = press.user.mention(); + let admin = press.user; + formatter = formatter.user(&admin).text(&content); + let is_approved = press.data.custom_id == approve_id; let content = if is_approved { - format!("{content}\nApproved by: {admin}") + formatter.fmt(&strings.answer.accepted_by) } else { - format!("~~{content}~~\nRejected by: {admin}") + formatter.fmt(&strings.answer.rejected_by) }; let builder = EditMessage::new().content(content).components(Vec::new()); @@ -122,21 +131,15 @@ pub async fn answer( no_errors = false; }; + formatter = formatter.current_balance(&account); + if no_errors { - content = format!("Your answer to the quest #{quest_id} has been approved.\n\ - You gained {reward} points.\n\ - Your balance is now {balance} points", - reward = quest.reward, - balance = account.balance - ); + content = formatter.fmt(&strings.answer.reply.accepted); } else { - content = format!("Your answer to the quest #{quest_id} has been approved, \ - but some server error happened. \ - Please contact administrator for details." - ); + content = formatter.fmt(&strings.answer.reply.error); } } else { - content = format!("Your answer to the quest #{quest_id} has been rejected."); + content = formatter.fmt(&strings.answer.reply.rejected); }; let dm_builder = CreateMessage::new().content(content); ctx.author().dm(ctx, dm_builder).await?; diff --git a/discord/src/commands/init.rs b/discord/src/commands/init.rs index e642921..aac39a2 100644 --- a/discord/src/commands/init.rs +++ b/discord/src/commands/init.rs @@ -30,7 +30,9 @@ pub async fn init( let path = &ctx.data().config.full_impl_path().unwrap(); guard.save(path.parent().unwrap_or(Path::new("")).to_owned())? }; - let reply_string = "Settings updated.".to_string(); + let strings = &ctx.data().strings; + let formatter = strings.formatter(); + let reply_string = formatter.fmt(&strings.init_reply); ctx.reply(reply_string).await?; Ok(()) diff --git a/discord/src/commands/map.rs b/discord/src/commands/map.rs index efa58f7..38c6a0c 100644 --- a/discord/src/commands/map.rs +++ b/discord/src/commands/map.rs @@ -31,8 +31,14 @@ pub async fn unlock( let account_path = conf.full_accounts_path(); account.save(account_path)?; + + let strings = &ctx.data().strings; + let formatter = strings.formatter() + .user(ctx.author()) + .balance(&account, &map) + .value(id); - let reply_string = format!("Unlocked room #{id}. Your balance: {} points", account.balance); + let reply_string = formatter.fmt(&strings.map.room_unlocked); ctx.reply(reply_string).await?; Ok(()) @@ -59,8 +65,13 @@ pub async fn r#move( account.location = id; let account_path = conf.full_accounts_path(); account.save(account_path)?; + + let strings = &ctx.data().strings; + let formatter = strings.formatter() + .user(ctx.author()) + .value(id); - let reply_string = format!("Moved to room #{id}."); + let reply_string = formatter.fmt(&strings.map.moved_to_room); ctx.reply(reply_string).await?; Ok(()) diff --git a/discord/src/commands/social.rs b/discord/src/commands/social.rs index 118b593..33cb2d5 100644 --- a/discord/src/commands/social.rs +++ b/discord/src/commands/social.rs @@ -1,4 +1,4 @@ -use poise::serenity_prelude::{Attachment, ChannelId, CreateAttachment, CreateMessage, EditMessage, Mentionable, MessageId, UserId}; +use poise::serenity_prelude::{Attachment, ChannelId, CreateAttachment, CreateMessage, EditMessage, MessageId, UserId}; use crate::{Context, Error}; @@ -51,20 +51,22 @@ pub async fn msg ( builder = builder.add_file(attachment); } + let strings = &ctx.data().strings; + let reply_string = if let Some(channel) = channel { let message = channel.send_message(ctx, builder).await?; - format!("Sent {message} ({message_id}) to {channel}!", - message = message.link(), - message_id = message.id, - channel = channel.mention(), - ) - } else if let Some(user) = user { - let message = user.dm(ctx, builder).await?; - format!("Sent {message} ({message_id}) to {user}", - message = message.link(), - message_id = message.id, - user = user.mention(), - ) + let formatter = strings.formatter() + .message(&message); + + formatter.fmt(&strings.social.sent_channel) + } else if let Some(user_id) = user { + let message = user_id.dm(ctx, builder).await?; + let user = user_id.to_user(ctx).await?; + let formatter = strings.formatter() + .message(&message) + .user(&user); + + formatter.fmt(&strings.social.sent_dm) } else { unreachable!(); }; @@ -113,16 +115,23 @@ pub async fn edit ( builder = builder.new_attachment(attachment); } + let mut message; if let Some(channel) = channel { - let mut message = channel.message(ctx, message_id).await?; + message = channel.message(ctx, message_id).await?; message.edit(ctx, builder).await?; } else if let Some(user) = user { let channel = user.create_dm_channel(ctx).await?; - let mut message = channel.message(ctx, message_id).await?; + message = channel.message(ctx, message_id).await?; message.edit(ctx, builder).await?; + } else { + unreachable!() } - let reply_string = "Successfully edited message.".to_string(); + let strings = &ctx.data().strings; + let formatter = strings.formatter() + .message(&message); + + let reply_string = formatter.fmt(&strings.social.edited); ctx.reply(reply_string).await?; Ok(()) @@ -149,17 +158,24 @@ pub async fn undo( if channel.is_some() && user.is_some() { return Err(Error::BothChannelAndUser); } - + + let message; if let Some(channel) = channel { - let message = channel.message(ctx, message_id).await?; + message = channel.message(ctx, message_id).await?; message.delete(ctx).await?; } else if let Some(user) = user { let channel = user.create_dm_channel(ctx).await?; - let message = channel.message(ctx, message_id).await?; + message = channel.message(ctx, message_id).await?; message.delete(ctx).await?; + } else { + unreachable!() } + + let strings = &ctx.data().strings; + let formatter = strings.formatter() + .message(&message); - let reply_string = "Successfully deleted message".to_string(); + let reply_string = formatter.fmt(&strings.social.deleted); ctx.reply(reply_string).await?; Ok(()) diff --git a/discord/src/error.rs b/discord/src/error.rs index 1cb3927..e5f57c8 100644 --- a/discord/src/error.rs +++ b/discord/src/error.rs @@ -15,6 +15,7 @@ pub enum Error { SerenityError(serenity::Error), SquadQuestError(squad_quest::error::Error), AccountNotFound, + AccountIsSelf, InsufficientFunds(u32), RoomNotFound(u16), RoomAlreadyUnlocked(u16), @@ -57,6 +58,7 @@ impl Display for Error { Self::SerenityError(_) => write!(f, "discord interaction error"), Self::SquadQuestError(_) => write!(f, "internal logic error"), Self::AccountNotFound => write!(f, "account not found"), + Self::AccountIsSelf => write!(f, "given account is the same as command user"), Self::InsufficientFunds(amount) => write!(f, "user does not have {amount} points"), Self::RoomNotFound(id) => write!(f, "room #{id} not found"), Self::RoomAlreadyUnlocked(id) => write!(f, "room #{id} is already unlocked for this user"), @@ -75,6 +77,7 @@ impl std::error::Error for Error { Self::NoChannelOrUser | Self::BothChannelAndUser | Self::AccountNotFound | + Self::AccountIsSelf | Self::InsufficientFunds(_) | Self::RoomNotFound(_) | Self::RoomAlreadyUnlocked(_) | diff --git a/discord/src/strings.rs b/discord/src/strings.rs index d31c6e0..711afc9 100644 --- a/discord/src/strings.rs +++ b/discord/src/strings.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, fmt::Display, io::Write, path::PathBuf}; -use poise::serenity_prelude::{Mentionable, User}; +use poise::serenity_prelude::{Mentionable, Message, User}; use serde::{Deserialize, Serialize}; use squad_quest::{SquadObject, account::Account, map::Map, quest::{Quest, QuestDifficulty}, error::Error}; @@ -58,8 +58,8 @@ impl StringFormatter { self.with_tags(new_tags) } - pub fn balance(self, account: &Account, map: &Map) -> Self { - let balance = ("{b.current}".to_string(), self.points(account.balance)); + pub fn balance(mut self, account: &Account, map: &Map) -> Self { + self = self.current_balance(account); let full_balance = ( "{b.full}".to_string(), self.points(account_full_balance(account, map)), @@ -68,7 +68,21 @@ impl StringFormatter { "{b.rooms}".to_string(), self.points(account_rooms_value(account, map)), ); - let new_tags = vec![ balance, full_balance, rooms_balance ]; + let new_tags = vec![ full_balance, rooms_balance ]; + + self.with_tags(new_tags) + } + + pub fn current_balance(self, account: &Account) -> Self { + let balance = ("{b.current}".to_string(), self.points(account.balance)); + self.with_tags(vec![balance]) + } + + pub fn message(self, message: &Message) -> Self { + let link = ("{m.link}".to_string(), message.link()); + let id = ("{m.id}".to_string(), message.id.to_string()); + let channel = ("{m.channel}".to_string(), message.channel_id.mention().to_string()); + let new_tags = vec![ link, id, channel ]; self.with_tags(new_tags) } @@ -76,7 +90,7 @@ impl StringFormatter { pub fn text(self, text: impl ToString) -> Self { let text = ("{text}".to_string(), text.to_string()); - self.with_tags(vec![text]) +self.with_tags(vec![text]) } pub fn value(self, value: impl ToString) -> Self { @@ -110,9 +124,13 @@ pub struct Strings { pub url: String, pub points: String, pub info: String, + pub init_reply: String, + pub account: AccountReplies, pub answer: Answer, pub difficulty: Difficulty, + pub map: MapReplies, pub scoreboard: Scoreboard, + pub social: Social, pub quest: QuestStrings, } @@ -123,10 +141,14 @@ impl Default for Strings { points: "points".to_string(), info: "SquadQuest version {v}\ {n}Find the map here: {url}".to_string(), + init_reply: "Updated linked channels and guild.".to_string(), answer: Answer::default(), difficulty: Difficulty::default(), scoreboard: Scoreboard::default(), quest: QuestStrings::default(), + social: Social::default(), + account: AccountReplies::default(), + map: MapReplies::default(), } } } @@ -192,6 +214,7 @@ pub struct Answer { pub attachment_notice: String, pub accepted_by: String, pub rejected_by: String, + pub reply: AnswerReplies, } impl Default for Answer { @@ -200,10 +223,35 @@ impl Default for Answer { from: "## From: {u.mention}{n}".to_string(), quest: "### Quest {q.id}: {q.name}{n}".to_string(), expected: "### Expected answer:{n}||{q.answer}||".to_string(), - text: "### Passed answer:{n}{text}".to_string(), - attachment_notice: "Passed answer has attachments.".to_string(), + text: "{n}### Passed answer:{n}{text}".to_string(), + attachment_notice: "{n}Passed answer has attachments.".to_string(), accepted_by: "{text}{n}Accepted by: {u.mention}".to_string(), rejected_by: "~~{text}~~{n}Rejected by: {u.mention}".to_string(), + reply: AnswerReplies::default(), + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(default)] +pub struct AnswerReplies { + pub initial: String, + pub accepted: String, + pub rejected: String, + pub error: String, +} + +impl Default for AnswerReplies { + fn default() -> Self { + Self { + initial: "Your answer has been posted.".to_string(), + accepted: "Your answer to the quest {q.id} has been approved.{n}\ + You gained: {q.reward}{n}\ + Your current balance is {b.current}.".to_string(), + rejected: "Your answer to the quest {q.id} has been rejected.".to_string(), + error: "Your answer to the quest {q.id} has been approved, \ + but some server error happened. \ + Please contact administator for details.".to_string(), } } } @@ -240,6 +288,7 @@ match difficulty { } #[derive(Deserialize, Serialize, Debug)] +#[serde(default)] pub struct Scoreboard { pub header: String, pub line_format: String, @@ -258,6 +307,7 @@ impl Default for Scoreboard { } #[derive(Deserialize, Serialize, Debug)] +#[serde(default)] pub struct QuestStrings { pub list: String, pub list_item: String, @@ -285,3 +335,58 @@ impl Default for QuestStrings { } } } + +#[derive(Deserialize, Serialize, Debug)] +#[serde(default)] +pub struct Social { + pub sent_channel: String, + pub sent_dm: String, + pub edited: String, + pub deleted: String, +} + +impl Default for Social { + fn default() -> Self { + Self { + sent_channel: "Sent {m.link} ({m.id}) to {m.channel}".to_string(), + sent_dm: "Sent {m.link} ({m.id}) to {u.mention}".to_string(), + edited: "Edited message {m.id}".to_string(), + deleted: "Deleted message {m.id}".to_string(), + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(default)] +pub struct AccountReplies { + pub reset: String, + pub give_pt: String, + pub set_pt: String, +} + +impl Default for AccountReplies { + fn default() -> Self { + Self { + reset: "Reset {u.name} account".to_string(), + give_pt: "Given {value} {pt} to {u.name}{n}\ + Your current balance: {b.current}".to_string(), + set_pt: "Set {u.name} balance to {b.current}".to_string(), + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(default)] +pub struct MapReplies { + pub room_unlocked: String, + pub moved_to_room: String, +} + +impl Default for MapReplies { + fn default() -> Self { + Self { + room_unlocked: "Unlocked room #{value}. Your balance: {b.current}".to_string(), + moved_to_room: "Moved to room #{value}".to_string(), + } + } +}