From bcc57087cea9b795286cdf596aa3591b58c812da Mon Sep 17 00:00:00 2001 From: Ahmad <103906421+ahmadk953@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:14:28 -0400 Subject: [PATCH 1/3] fix(bot): fixed achievement command --- src/commands/fun/achievement.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/commands/fun/achievement.ts b/src/commands/fun/achievement.ts index c1ef61b..5fea0f2 100644 --- a/src/commands/fun/achievement.ts +++ b/src/commands/fun/achievement.ts @@ -26,7 +26,6 @@ const command = { data: new SlashCommandBuilder() .setName('achievement') .setDescription('Manage server achievements') - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) .addSubcommand((subcommand) => subcommand .setName('create') @@ -185,6 +184,13 @@ async function handleCreateAchievement( const rewardType = interaction.options.getString('reward_type'); const rewardValue = interaction.options.getString('reward_value'); + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { + await interaction.editReply( + 'You do not have permission to create achievements.', + ); + return; + } + if (requirementType === 'command_usage' && !commandName) { await interaction.editReply( 'Command name is required for command_usage type achievements.', @@ -252,6 +258,13 @@ async function handleDeleteAchievement( ) { const achievementId = interaction.options.getInteger('id')!; + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { + await interaction.editReply( + 'You do not have permission to delete achievements.', + ); + return; + } + try { const success = await deleteAchievement(achievementId); @@ -278,6 +291,13 @@ async function handleAwardAchievement( const user = interaction.options.getUser('user')!; const achievementId = interaction.options.getInteger('achievement_id')!; + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { + await interaction.editReply( + 'You do not have permission to award achievements.', + ); + return; + } + try { const allAchievements = await getAllAchievements(); const achievement = allAchievements.find((a) => a.id === achievementId); @@ -659,6 +679,13 @@ async function handleUnawardAchievement( const user = interaction.options.getUser('user')!; const achievementId = interaction.options.getInteger('achievement_id')!; + if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { + await interaction.editReply( + 'You do not have permission to unaward achievements.', + ); + return; + } + try { const allAchievements = await getAllAchievements(); const achievement = allAchievements.find((a) => a.id === achievementId); From 86e0a59188a93c293cc004b22d61386736aeec68 Mon Sep 17 00:00:00 2001 From: Ahmad <103906421+ahmadk953@users.noreply.github.com> Date: Thu, 19 Jun 2025 23:28:16 -0400 Subject: [PATCH 2/3] fix(bot): fixed duplicate achievement sending --- .github/FUNDING.yml | 1 - src/util/achievementManager.ts | 217 ++++++++++----------------------- 2 files changed, 64 insertions(+), 154 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 01ce41d..31791dc 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,2 @@ github: ahmadk953 -patreon: poixpixel thanks_dev: u/gh/ahmadk953 diff --git a/src/util/achievementManager.ts b/src/util/achievementManager.ts index adb78d9..d780545 100644 --- a/src/util/achievementManager.ts +++ b/src/util/achievementManager.ts @@ -1,11 +1,4 @@ -import { - Message, - Client, - EmbedBuilder, - GuildMember, - TextChannel, - Guild, -} from 'discord.js'; +import { Message, EmbedBuilder, TextChannel, Guild } from 'discord.js'; import { addXpToUser, @@ -21,99 +14,65 @@ import { loadConfig } from './configLoader.js'; import { generateAchievementCard } from './achievementCardGenerator.js'; /** - * Check and process achievements for a user based on a message - * @param message - The message that triggered the check + * Handle achievement progress updates + * @param userId - ID of the user + * @param guild - Guild instance (can be null if not applicable) + * @param achievement - Achievement definition + * @param progress - Progress percentage (0-100) + * @param options - Additional options + */ +async function handleProgress( + userId: string, + guild: Guild | null, + achievement: schema.achievementDefinitionsTableTypes, + progress: number, + options: { skipAward?: boolean } = {}, +): Promise { + const { skipAward = false } = options; + const userAchievements = await getUserAchievements(userId); + const existing = userAchievements.find( + (a) => a.achievementId === achievement.id && a.earnedAt !== null, + ); + + if (progress >= 100) { + if (!existing && !skipAward) { + const awarded = await awardAchievement(userId, achievement.id); + if (awarded && guild) { + await announceAchievement(guild, userId, achievement); + } + } + } else { + await updateAchievementProgress(userId, achievement.id, progress); + } +} + +/** + * Process message achievements based on user activity + * @param message - The message object from Discord */ export async function processMessageAchievements( message: Message, ): Promise { if (message.author.bot) return; - const userData = await getUserLevel(message.author.id); const allAchievements = await getAllAchievements(); - const messageAchievements = allAchievements.filter( + for (const ach of allAchievements.filter( (a) => a.requirementType === 'message_count', - ); - - for (const achievement of messageAchievements) { + )) { const progress = Math.min( 100, - (userData.messagesSent / achievement.threshold) * 100, + (userData.messagesSent / ach.threshold) * 100, ); - - if (progress >= 100) { - const userAchievements = await getUserAchievements(message.author.id); - const existingAchievement = userAchievements.find( - (a) => a.achievementId === achievement.id && a.earnedAt !== null, - ); - - if (!existingAchievement) { - const awarded = await awardAchievement( - message.author.id, - achievement.id, - ); - if (awarded) { - await announceAchievement( - message.guild!, - message.author.id, - achievement, - ); - } - } - } else { - await updateAchievementProgress( - message.author.id, - achievement.id, - progress, - ); - } - } - - const levelAchievements = allAchievements.filter( - (a) => a.requirementType === 'level', - ); - - for (const achievement of levelAchievements) { - const progress = Math.min( - 100, - (userData.level / achievement.threshold) * 100, - ); - - if (progress >= 100) { - const userAchievements = await getUserAchievements(message.author.id); - const existingAchievement = userAchievements.find( - (a) => a.achievementId === achievement.id && a.earnedAt !== null, - ); - - if (!existingAchievement) { - const awarded = await awardAchievement( - message.author.id, - achievement.id, - ); - if (awarded) { - await announceAchievement( - message.guild!, - message.author.id, - achievement, - ); - } - } - } else { - await updateAchievementProgress( - message.author.id, - achievement.id, - progress, - ); - } + await handleProgress(message.author.id, message.guild!, ach, progress); } } /** - * Check achievements for level-ups - * @param memberId - Member ID who leveled up - * @param newLevel - New level value - * @guild - Guild instance + * Process level-up achievements when a user levels up + * @param memberId - ID of the member who leveled up + * @param newLevel - The new level the member has reached + * @param guild - Guild instance where the member belongs */ export async function processLevelUpAchievements( memberId: string, @@ -121,37 +80,19 @@ export async function processLevelUpAchievements( guild: Guild, ): Promise { const allAchievements = await getAllAchievements(); - - const levelAchievements = allAchievements.filter( + for (const ach of allAchievements.filter( (a) => a.requirementType === 'level', - ); - - for (const achievement of levelAchievements) { - const progress = Math.min(100, (newLevel / achievement.threshold) * 100); - - if (progress >= 100) { - const userAchievements = await getUserAchievements(memberId); - const existingAchievement = userAchievements.find( - (a) => a.achievementId === achievement.id && a.earnedAt !== null, - ); - - if (!existingAchievement) { - const awarded = await awardAchievement(memberId, achievement.id); - if (awarded) { - await announceAchievement(guild, memberId, achievement); - } - } - } else { - await updateAchievementProgress(memberId, achievement.id, progress); - } + )) { + const progress = Math.min(100, (newLevel / ach.threshold) * 100); + await handleProgress(memberId, guild, ach, progress); } } /** - * Process achievements for command usage - * @param userId - User ID who used the command - * @param commandName - Name of the command - * @param client - Guild instance + * Process command usage achievements when a command is invoked + * @param userId - ID of the user who invoked the command + * @param commandName - Name of the command invoked + * @param guild - Guild instance where the command was invoked */ export async function processCommandAchievements( userId: string, @@ -159,7 +100,6 @@ export async function processCommandAchievements( guild: Guild, ): Promise { const allAchievements = await getAllAchievements(); - const commandAchievements = allAchievements.filter( (a) => a.requirementType === 'command_usage' && @@ -167,26 +107,16 @@ export async function processCommandAchievements( (a.requirement as any).command === commandName, ); - for (const achievement of commandAchievements) { - const userAchievements = await getUserAchievements(userId); - const existingAchievement = userAchievements.find( - (a) => a.achievementId === achievement.id && a.earnedAt !== null, - ); - - if (!existingAchievement) { - const awarded = await awardAchievement(userId, achievement.id); - if (awarded) { - await announceAchievement(guild, userId, achievement); - } - } + for (const ach of commandAchievements) { + await handleProgress(userId, guild, ach, 100); } } /** - * Process achievements for reaction events (add or remove) - * @param userId - User ID who added/removed the reaction - * @param guild - Guild instance - * @param isRemoval - Whether this is a reaction removal (true) or addition (false) + * Process reaction achievements when a user reacts to a message + * @param userId - ID of the user who reacted + * @param guild - Guild instance where the reaction occurred + * @param isRemoval - Whether the reaction was removed (default: false) */ export async function processReactionAchievements( userId: string, @@ -198,40 +128,21 @@ export async function processReactionAchievements( if (member.user.bot) return; const allAchievements = await getAllAchievements(); - const reactionAchievements = allAchievements.filter( (a) => a.requirementType === 'reactions', ); - - if (reactionAchievements.length === 0) return; + if (!reactionAchievements.length) return; const reactionCount = await getUserReactionCount(userId); - for (const achievement of reactionAchievements) { + for (const ach of reactionAchievements) { const progress = Math.max( 0, - Math.min(100, (reactionCount / achievement.threshold) * 100), + Math.min(100, (reactionCount / ach.threshold) * 100), ); - - if (progress >= 100 && !isRemoval) { - const userAchievements = await getUserAchievements(userId); - const existingAchievement = userAchievements.find( - (a) => - a.achievementId === achievement.id && - a.earnedAt !== null && - a.earnedAt !== undefined && - new Date(a.earnedAt).getTime() > 0, - ); - - if (!existingAchievement) { - const awarded = await awardAchievement(userId, achievement.id); - if (awarded) { - await announceAchievement(guild, userId, achievement); - } - } - } - - await updateAchievementProgress(userId, achievement.id, progress); + await handleProgress(userId, guild, ach, progress, { + skipAward: isRemoval, + }); } } catch (error) { console.error('Error processing reaction achievements:', error); From d71d9f0dccb0d2b35f520f47d32f69df17cf9d12 Mon Sep 17 00:00:00 2001 From: Ahmad <103906421+ahmadk953@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:07:15 -0400 Subject: [PATCH 3/3] fix(bot): fixed command achievements and progress updates --- src/db/functions/achievementFunctions.ts | 13 +++------- src/util/achievementManager.ts | 31 +++++++++++++++++------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/db/functions/achievementFunctions.ts b/src/db/functions/achievementFunctions.ts index 30cbc3b..7d3ee6a 100644 --- a/src/db/functions/achievementFunctions.ts +++ b/src/db/functions/achievementFunctions.ts @@ -129,7 +129,6 @@ export async function updateAchievementProgress( ): Promise { try { await ensureDbInitialized(); - if (!db) { console.error( 'Database not initialized, cannot update achievement progress', @@ -149,21 +148,15 @@ export async function updateAchievementProgress( .then((rows) => rows[0]); if (existing) { - if (existing.earnedAt) { - return false; - } - await db .update(schema.userAchievementsTable) - .set({ - progress: Math.floor(progress) > 100 ? 100 : Math.floor(progress), - }) + .set({ progress }) .where(eq(schema.userAchievementsTable.id, existing.id)); } else { await db.insert(schema.userAchievementsTable).values({ discordId: userId, - achievementId: achievementId, - progress: Math.floor(progress) > 100 ? 100 : Math.floor(progress), + achievementId, + progress, }); } diff --git a/src/util/achievementManager.ts b/src/util/achievementManager.ts index d780545..aaf3a05 100644 --- a/src/util/achievementManager.ts +++ b/src/util/achievementManager.ts @@ -34,15 +34,13 @@ async function handleProgress( (a) => a.achievementId === achievement.id && a.earnedAt !== null, ); - if (progress >= 100) { - if (!existing && !skipAward) { - const awarded = await awardAchievement(userId, achievement.id); - if (awarded && guild) { - await announceAchievement(guild, userId, achievement); - } + await updateAchievementProgress(userId, achievement.id, progress); + + if (progress === 100 && !existing && !skipAward) { + const awarded = await awardAchievement(userId, achievement.id); + if (awarded && guild) { + await announceAchievement(guild, userId, achievement); } - } else { - await updateAchievementProgress(userId, achievement.id, progress); } } @@ -107,8 +105,23 @@ export async function processCommandAchievements( (a.requirement as any).command === commandName, ); + // fetch the user’s current achievement entries + const userAchievements = await getUserAchievements(userId); + for (const ach of commandAchievements) { - await handleProgress(userId, guild, ach, 100); + // find existing progress, default to 0 + const userAch = userAchievements.find((u) => u.achievementId === ach.id); + const oldProgress = userAch?.progress ?? 0; + + // compute how many times they've run this command so far + const timesRanSoFar = (oldProgress / 100) * ach.threshold; + const newCount = timesRanSoFar + 1; + + // convert back into a percentage + const newProgress = Math.min(100, (newCount / ach.threshold) * 100); + + // Delegate to handleProgress which will update or award + await handleProgress(userId, guild, ach, newProgress); } }