1+ // apps/bot/commands/badges.ts
2+
3+ import {
4+ SlashCommandBuilder ,
5+ ChatInputCommandInteraction ,
6+ EmbedBuilder ,
7+ PermissionFlagsBits ,
8+ MessageFlags ,
9+ } from "discord.js" ;
10+ import { supabaseBot } from "../utils/supabase.bot" ;
11+
12+ //Slash command
13+ const slashCommand = new SlashCommandBuilder ( )
14+ . setName ( "badges" )
15+ . setDescription ( "View your badges or award a badge (admin only)." )
16+ . addUserOption ( ( opt ) =>
17+ opt
18+ . setName ( "user" )
19+ . setDescription ( "Target user to view or award badges to." )
20+ . setRequired ( false )
21+ )
22+ . addStringOption ( ( opt ) =>
23+ opt
24+ . setName ( "badge" )
25+ . setDescription ( "Badge ID to award (admin only)" )
26+ . setRequired ( false )
27+ ) ;
28+
29+ // Export the command
30+ export const command = {
31+ name : "badges" ,
32+ data : slashCommand ,
33+ } ;
34+
35+ // Execute function
36+ export const execute = async ( interaction : ChatInputCommandInteraction ) => {
37+ try {
38+ await interaction . deferReply ( ) ;
39+
40+ const userOption = interaction . options . getUser ( "user" ) ;
41+ const badgeId = interaction . options . getString ( "badge" ) ;
42+ const targetUser = userOption || interaction . user ;
43+
44+ // Ensure users_discord record exists for target user
45+ const { error : userError } = await supabaseBot
46+ . from ( "users_discord" )
47+ . upsert (
48+ {
49+ id : targetUser . id ,
50+ username : targetUser . username ,
51+ avatar_url : targetUser . displayAvatarURL ( ) ,
52+ discriminator : targetUser . discriminator || "0" ,
53+ } ,
54+ { onConflict : "id" }
55+ ) ;
56+
57+ if ( userError ) {
58+ console . error ( "❌ Error ensuring user record:" , userError . message ) ;
59+ }
60+
61+ // ===== ADMIN AWARD FLOW =====
62+ if ( badgeId ) {
63+ // Permission check
64+ if (
65+ ! interaction . memberPermissions ?. has ( PermissionFlagsBits . ManageGuild )
66+ ) {
67+ await interaction . editReply ( {
68+ content :
69+ "❌ You do not have permission to award badges. (Requires **Manage Server** permission.)" ,
70+ } ) ;
71+ return ;
72+ }
73+
74+ // Validate badgeId
75+ const badgeIdNum = parseInt ( badgeId ) ;
76+ if ( isNaN ( badgeIdNum ) ) {
77+ await interaction . editReply ( {
78+ content : "❌ Invalid badge ID. Please provide a valid **number**." ,
79+ } ) ;
80+ return ;
81+ }
82+
83+ // Check if badge exists
84+ const { data : badge , error : badgeError } = await supabaseBot
85+ . from ( "badges_discord" )
86+ . select ( "id, badge_name" )
87+ . eq ( "id" , badgeIdNum )
88+ . single ( ) ;
89+
90+ if ( badgeError || ! badge ) {
91+ await interaction . editReply ( {
92+ content : `❌ No badge found with ID **${ badgeIdNum } **.` ,
93+ } ) ;
94+ return ;
95+ }
96+
97+ // Check if user already has this badge
98+ const { data : existingBadge } = await supabaseBot
99+ . from ( "user_badges_discord" )
100+ . select ( "id" )
101+ . eq ( "user_id" , targetUser . id )
102+ . eq ( "badge_id" , badgeIdNum )
103+ . maybeSingle ( ) ;
104+
105+ if ( existingBadge ) {
106+ await interaction . editReply ( {
107+ content : `⚠️ <@${ targetUser . id } > already has the badge **${ badge . badge_name } **.` ,
108+ } ) ;
109+ return ;
110+ }
111+
112+ // Award the badge
113+ const { error : insertError } = await supabaseBot
114+ . from ( "user_badges_discord" )
115+ . insert ( {
116+ user_id : targetUser . id ,
117+ badge_id : badgeIdNum ,
118+ } ) ;
119+
120+ if ( insertError ) {
121+ console . error ( "❌ Error awarding badge:" , insertError . message ) ;
122+ await interaction . editReply ( {
123+ content : "⚠️ Failed to award badge. Please try again later." ,
124+ } ) ;
125+ return ;
126+ }
127+
128+ await interaction . editReply ( {
129+ content : `🏅 **${ badge . badge_name } ** has been awarded to <@${ targetUser . id } >!` ,
130+ } ) ;
131+
132+ console . log (
133+ `✅ Badge ${ badgeIdNum } awarded to ${ targetUser . username } by ${ interaction . user . username } `
134+ ) ;
135+ return ;
136+ }
137+
138+ // ===== VIEW BADGES FLOW =====
139+ const { data : userBadges , error : ubError } = await supabaseBot
140+ . from ( "user_badges_discord" )
141+ . select ( "badge_id, awarded_at" )
142+ . eq ( "user_id" , targetUser . id ) ;
143+
144+ if ( ubError ) {
145+ console . error ( "❌ Error fetching user badges:" , ubError . message ) ;
146+ await interaction . editReply ( {
147+ content : "⚠️ Failed to fetch user badges. Please try again." ,
148+ } ) ;
149+ return ;
150+ }
151+
152+ if ( ! userBadges || userBadges . length === 0 ) {
153+ await interaction . editReply ( {
154+ content : `<@${ targetUser . id } > has no badges yet. 🏆` ,
155+ } ) ;
156+ return ;
157+ }
158+
159+ // Fetch all badge details from badges_discord
160+ const { data : allBadges , error : allBadgesError } = await supabaseBot
161+ . from ( "badges_discord" )
162+ . select ( "id, badge_name, badge_icon_url, description" ) ;
163+
164+ if ( allBadgesError || ! allBadges ) {
165+ console . error ( "❌ Error fetching badge details:" , allBadgesError . message ) ;
166+ await interaction . editReply ( {
167+ content : "⚠️ Failed to load badge details. Please try again." ,
168+ } ) ;
169+ return ;
170+ }
171+
172+ // Merge badge info
173+ const badgesWithDetails = userBadges
174+ . map ( ( ub ) => {
175+ const badge = allBadges . find ( ( b ) => b . id === ub . badge_id ) ;
176+ if ( ! badge ) return null ;
177+ return {
178+ badge_name : badge . badge_name ,
179+ badge_icon_url : badge . badge_icon_url ,
180+ description : badge . description ,
181+ awarded_at : ub . awarded_at ,
182+ } ;
183+ } )
184+ . filter ( ( badge ) : badge is NonNullable < typeof badge > => badge !== null ) ;
185+
186+ // ===== CREATE EMBED =====
187+ const embed = new EmbedBuilder ( )
188+ . setTitle ( `${ targetUser . username } 's Badges` )
189+ . setColor ( "#FFD700" )
190+ . setThumbnail ( targetUser . displayAvatarURL ( ) )
191+ . setFooter ( { text : `Total Badges: ${ badgesWithDetails . length } ` } )
192+ . setTimestamp ( ) ;
193+
194+ if ( badgesWithDetails . length > 0 ) {
195+ embed . setDescription (
196+ badgesWithDetails
197+ . map (
198+ ( b ) =>
199+ `🏅 **${ b . badge_name } **\n${ b . description ?? "No description." } `
200+ )
201+ . join ( "\n\n" )
202+ ) ;
203+ } else {
204+ embed . setDescription ( "No badges found." ) ;
205+ }
206+
207+ await interaction . editReply ( { embeds : [ embed ] } ) ;
208+ } catch ( error ) {
209+ console . error ( "❌ Error executing badges command:" , error ) ;
210+
211+ const errorMessage = {
212+ content :
213+ "❌ An unexpected error occurred while processing this command. Please try again later." ,
214+ } ;
215+
216+ if ( interaction . deferred ) {
217+ await interaction . editReply ( errorMessage ) ;
218+ } else if ( ! interaction . replied ) {
219+ await interaction . reply ( {
220+ ...errorMessage ,
221+ flags : [ MessageFlags . Ephemeral ] ,
222+ } ) ;
223+ }
224+ }
225+ } ;
0 commit comments