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