giveaway.ts

1import {
2	ChatInputCommandInteraction,
3	SlashCommandBuilder,
4	EmbedBuilder,
5} from 'discord.js'
6import { Command } from '@/types/bot'
7import { supabase } from '@/configs/supabase'
8import ms from 'ms'
9
10export default {
11	data: new SlashCommandBuilder()
12		.setName('giveaway')
13		.setDescription('Start or manage giveaways')
14		.addSubcommand((subcommand) =>
15			subcommand
16				.setName('start')
17				.setDescription('Start a new giveaway')
18				.addStringOption((option) =>
19					option
20						.setName('prize')
21						.setDescription('What are you giving away?')
22						.setRequired(true),
23				)
24				.addStringOption((option) =>
25					option
26						.setName('duration')
27						.setDescription('How long should the giveaway last? (e.g., 1h, 1d)')
28						.setRequired(true),
29				)
30				.addIntegerOption((option) =>
31					option
32						.setName('winners')
33						.setDescription('Number of winners')
34						.setMinValue(1)
35						.setMaxValue(10)
36						.setRequired(false),
37				),
38		)
39		.addSubcommand((subcommand) =>
40			subcommand
41				.setName('end')
42				.setDescription('End a giveaway early')
43				.addStringOption((option) =>
44					option
45						.setName('message_id')
46						.setDescription('ID of the giveaway message')
47						.setRequired(true),
48				),
49		),
50
51	async execute(interaction: ChatInputCommandInteraction) {
52		const subcommand = interaction.options.getSubcommand()
53
54		if (subcommand === 'start') {
55			const prize = interaction.options.getString('prize', true)
56			const duration = interaction.options.getString('duration', true)
57			const winners = interaction.options.getInteger('winners') || 1
58
59			const durationMs = ms(duration)
60			if (!durationMs) {
61				return interaction.reply({
62					content: 'Invalid duration format! Use formats like: 1h, 30m, 1d',
63					ephemeral: true,
64				})
65			}
66
67			const endsAt = new Date(Date.now() + durationMs)
68
69			const embed = new EmbedBuilder()
70				.setTitle('🎉 GIVEAWAY 🎉')
71				.setDescription(
72					`
73          **Prize:** ${prize}
74          **Winners:** ${winners}
75          **Ends:** <t:${Math.floor(endsAt.getTime() / 1000)}:R>
76          
77          React with 🎉 to enter!
78        `,
79				)
80				.setColor('#FF9300')
81				.setFooter({ text: `Hosted by ${interaction.user.tag}` })
82
83			const message = await interaction.reply({
84				embeds: [embed],
85				fetchReply: true,
86			})
87			await message.react('🎉')
88
89			// Store giveaway in database
90			await supabase.from('giveaways').insert({
91				message_id: message.id,
92				channel_id: interaction.channelId,
93				prize,
94				winner_count: winners,
95				host_id: interaction.user.id,
96				ends_at: endsAt.toISOString(),
97			})
98		} else if (subcommand === 'end') {
99			const messageId = interaction.options.getString('message_id', true)
100			await endGiveaway(interaction, messageId)
101		}
102	},
103} as Command
104
105async function endGiveaway(
106	interaction: ChatInputCommandInteraction,
107	messageId: string,
108) {
109	const { data: giveaway } = await supabase
110		.from('giveaways')
111		.select('*')
112		.eq('message_id', messageId)
113		.single()
114
115	if (!giveaway || giveaway.ended) {
116		return interaction.reply({
117			content: 'Giveaway not found or already ended!',
118			ephemeral: true,
119		})
120	}
121
122	try {
123		const message = await interaction.channel?.messages.fetch(messageId)
124		if (!message) throw new Error('Message not found')
125
126		const reaction = message.reactions.cache.get('🎉')
127		if (!reaction) throw new Error('Reaction not found')
128
129		const users = await reaction.users.fetch()
130		const validParticipants = users.filter((user) => !user.bot)
131
132		if (validParticipants.size < 1) {
133			await interaction.reply('No valid participants found!')
134			return
135		}
136
137		const winners = validParticipants.random(
138			Math.min(giveaway.winner_count, validParticipants.size),
139		)
140
141		const winnerAnnouncement = new EmbedBuilder()
142			.setTitle('🎉 Giveaway Ended! 🎉')
143			.setDescription(
144				`
145        **Prize:** ${giveaway.prize}
146        **Winners:** ${winners.map((w) => `<@${w.id}>`).join(', ')}
147      `,
148			)
149			.setColor('#00FF00')
150			.setFooter({ text: `Giveaway ID: ${messageId}` })
151
152		await interaction.reply({ embeds: [winnerAnnouncement] })
153
154		// Update giveaway as ended
155		await supabase
156			.from('giveaways')
157			.update({ ended: true })
158			.eq('message_id', messageId)
159	} catch (error) {
160		await interaction.reply({
161			content: 'Failed to end giveaway!',
162			ephemeral: true,
163		})
164	}
165}
166