giveaway command problem

let me post
56 Replies
Sympact06
Sympact06โ€ข2mo ago
Creating a giveaway command with djs and sapphirejs so I do the command for example /giveaway prize: 1 winners: 3 duration: 1m then it would https://gyazo.com/7f489eb461275b0d452dd17e8958c59a
Gyazo
Gyazo
Sympact06
Sympact06โ€ข2mo ago
1m is 1 minute this shit is making 1 hour from it
Sympact06
Sympact06โ€ข2mo ago
Favna
Favnaโ€ข2mo ago
duration: 1m
that's a string yet you call const durationHours = parseInt(duration, 10);?
KaydaFox
KaydaFoxโ€ข2mo ago
If you use @sapphire/duration, it has a "Duration" class that will get the right values for you from human readable stuff That's the easy way around it imo
Favna
Favnaโ€ข2mo ago
yeah should be something like this wherein paramater is the argument provided
import { Duration } from '@sapphire/time-utilities';

const date = new Duration(parameter).fromNow;
if (!isNaN(date.getTime()) && date.getTime() > Date.now()) return date;
// throw a UserError
import { Duration } from '@sapphire/time-utilities';

const date = new Duration(parameter).fromNow;
if (!isNaN(date.getTime()) && date.getTime() > Date.now()) return date;
// throw a UserError
that's what we do for @Skyra as well I think your problem is that you're parsing 1m as if it doesn't include that m for an indicator of time unit actually looking at your messageRun method you did implement the splitting of time unit and number value there but you didn't do that for chatInputRun
Sympact06
Sympact06โ€ข2mo ago
I dont understand what the difference is for messagerun and chatinputrun
Favna
Favnaโ€ข2mo ago
one is for message based commands and the other is for slash commands message based commands being the old thing that we did before slash commands i.e. !giveaway whatever
Sympact06
Sympact06โ€ข2mo ago
Would this work then
import { Duration } from '@sapphire/time-utilities';

try {
const duration = new Duration(parameter);
const date = duration.fromNow;

if (!isNaN(date.getTime()) && date.getTime() > Date.now()) {
return date;
} else {
throw new Error('Invalid date: The computed date is either invalid or in the past.');
}
} catch (error) {
throw new UserError('Invalid duration format or unit. Please use a valid format like 1d, 1h, 1m, or 1s.');
}
import { Duration } from '@sapphire/time-utilities';

try {
const duration = new Duration(parameter);
const date = duration.fromNow;

if (!isNaN(date.getTime()) && date.getTime() > Date.now()) {
return date;
} else {
throw new Error('Invalid date: The computed date is either invalid or in the past.');
}
} catch (error) {
throw new UserError('Invalid duration format or unit. Please use a valid format like 1d, 1h, 1m, or 1s.');
}
Favna
Favnaโ€ข2mo ago
reduced to a bare minimum to save on characters, sample from it as you would
import { Duration } from '@sapphire/duration';
import { Command, UserError } from '@sapphire/framework';
import { EmbedBuilder } from 'discord.js';

export class GiveawayCommand extends Command {
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const durationOption = interaction.options.getString('duration', true);
const duration = new Duration(durationOption);
const endsAt = duration.fromNow;

if (isNaN(endsAt.getTime()) || endsAt.getTime() < Date.now()) {
throw new UserError({
identifier: 'InvalidDuration',
message: 'Invalid duration format or unit. Please use a valid format like 1d, 1h, 1m, or 1s.'
});
}

const embed = new EmbedBuilder()
.setTitle(prize)
.setDescription(`React with ๐ŸŽ‰ to enter!\nTime remaining: ${endsAt.toLocaleString()}\nWinners: ${winners}`)
.setFooter({ text: 'Ends at', iconURL: interaction.client.user?.displayAvatarURL() })
.setTimestamp(endsAt);

await interaction.reply({ embeds: [embed] });

// react with ๐ŸŽ‰
await interaction
.fetchReply()
.then((reply) => reply.react('๐ŸŽ‰'))
.catch(() => interaction.followUp('Failed to react with ๐ŸŽ‰').then(() => interaction.deleteReply()));
}
}
import { Duration } from '@sapphire/duration';
import { Command, UserError } from '@sapphire/framework';
import { EmbedBuilder } from 'discord.js';

export class GiveawayCommand extends Command {
public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
const durationOption = interaction.options.getString('duration', true);
const duration = new Duration(durationOption);
const endsAt = duration.fromNow;

if (isNaN(endsAt.getTime()) || endsAt.getTime() < Date.now()) {
throw new UserError({
identifier: 'InvalidDuration',
message: 'Invalid duration format or unit. Please use a valid format like 1d, 1h, 1m, or 1s.'
});
}

const embed = new EmbedBuilder()
.setTitle(prize)
.setDescription(`React with ๐ŸŽ‰ to enter!\nTime remaining: ${endsAt.toLocaleString()}\nWinners: ${winners}`)
.setFooter({ text: 'Ends at', iconURL: interaction.client.user?.displayAvatarURL() })
.setTimestamp(endsAt);

await interaction.reply({ embeds: [embed] });

// react with ๐ŸŽ‰
await interaction
.fetchReply()
.then((reply) => reply.react('๐ŸŽ‰'))
.catch(() => interaction.followUp('Failed to react with ๐ŸŽ‰').then(() => interaction.deleteReply()));
}
}
Sympact06
Sympact06โ€ข2mo ago
And btw , @Favna how would I trigger the end of a giveaway? Never did such thing
Favna
Favnaโ€ข2mo ago
store a reference (probably to the message people react to) somewhere, like in a database, and upon people executing /giveaway-end or a timer expiring stop collecting reactions and/or delete the message
Sympact06
Sympact06โ€ข2mo ago
I do save it in a db const giveaway = await prisma.giveaway.create({ data: { guildId: interaction.guildId!, channelId: interaction.channelId!, messageId: reply.id, endsAt, prize, winners, roles: rolesString, invites: invitesString }, }); so I did task/endGiveaway.ts with this but it aint working
import { container } from '@sapphire/framework';
import { PrismaClient } from '@prisma/client';
import { TextChannel, EmbedBuilder } from 'discord.js';
import { CronJob } from 'cron';

const prisma = new PrismaClient();

export class EndGiveawayTask {
private job: CronJob;

constructor() {
this.job = new CronJob('0 * * * * *', async () => {
await this.run();
}, null, true, 'America/New_York');
this.job.start();
}

public async run() {
const now = new Date();
const giveaways = await prisma.giveaway.findMany({
where: {
endsAt: { lte: now },
ended: false
}
});

for (const giveaway of giveaways) {
const channel = (container.client.channels.cache.get(giveaway.channelId) as TextChannel);
if (!channel) continue;

const message = await channel.messages.fetch(giveaway.messageId).catch(() => null);
if (!message) continue;

const reaction = message.reactions.cache.get('๐ŸŽ‰');
if (!reaction) continue;

const users = await reaction.users.fetch();
const entries = users.filter(user => !user.bot).map(user => user.id);

// Implement role weights and invites logic here if needed

const winners = this.pickWinners(entries, giveaway.winners);

// Announce winners
const winnerMentions = winners.map(userId => `<@${userId}>`).join(', ');
const embed = new EmbedBuilder()
.setTitle(`Giveaway Ended!`)
.setDescription(`Congratulations to the winners: ${winnerMentions}! You won **${giveaway.prize}**!`)
.setColor('#00FF00');

await channel.send({ embeds: [embed] });

// Update the giveaway as ended
await prisma.giveaway.update({
where: { id: giveaway.id },
data: { ended: true }
});
}
}

private pickWinners(entries: string[], count: number): string[] {
const winners = [];
for (let i = 0; i < count; i++) {
if (entries.length === 0) break;
const index = Math.floor(Math.random() * entries.length);
winners.push(entries.splice(index, 1)[0]);
}
return winners;
}
}
import { container } from '@sapphire/framework';
import { PrismaClient } from '@prisma/client';
import { TextChannel, EmbedBuilder } from 'discord.js';
import { CronJob } from 'cron';

const prisma = new PrismaClient();

export class EndGiveawayTask {
private job: CronJob;

constructor() {
this.job = new CronJob('0 * * * * *', async () => {
await this.run();
}, null, true, 'America/New_York');
this.job.start();
}

public async run() {
const now = new Date();
const giveaways = await prisma.giveaway.findMany({
where: {
endsAt: { lte: now },
ended: false
}
});

for (const giveaway of giveaways) {
const channel = (container.client.channels.cache.get(giveaway.channelId) as TextChannel);
if (!channel) continue;

const message = await channel.messages.fetch(giveaway.messageId).catch(() => null);
if (!message) continue;

const reaction = message.reactions.cache.get('๐ŸŽ‰');
if (!reaction) continue;

const users = await reaction.users.fetch();
const entries = users.filter(user => !user.bot).map(user => user.id);

// Implement role weights and invites logic here if needed

const winners = this.pickWinners(entries, giveaway.winners);

// Announce winners
const winnerMentions = winners.map(userId => `<@${userId}>`).join(', ');
const embed = new EmbedBuilder()
.setTitle(`Giveaway Ended!`)
.setDescription(`Congratulations to the winners: ${winnerMentions}! You won **${giveaway.prize}**!`)
.setColor('#00FF00');

await channel.send({ embeds: [embed] });

// Update the giveaway as ended
await prisma.giveaway.update({
where: { id: giveaway.id },
data: { ended: true }
});
}
}

private pickWinners(entries: string[], count: number): string[] {
const winners = [];
for (let i = 0; i < count; i++) {
if (entries.length === 0) break;
const index = Math.floor(Math.random() * entries.length);
winners.push(entries.splice(index, 1)[0]);
}
return winners;
}
}
Favna
Favnaโ€ข2mo ago
@Skyra used to have a giveaway module so you can dig through the git history if you want but we removed it because we came to the realization that we didn't want to bother competiting with Giveaway Bot which any large server is going to invite anyway because it had better features than our module
Favna
Favnaโ€ข2mo ago
https://github.com/skyra-project/skyra if you want to give it a shot
GitHub
GitHub - skyra-project/skyra: A multipurpose Discord Bot designed t...
A multipurpose Discord Bot designed to carry out most of your server's needs with great performance and stability. - skyra-project/skyra
Sympact06
Sympact06โ€ข2mo ago
cant find it rip 3000 commits
Favna
Favnaโ€ข2mo ago
GitHub
skyra/src/commands/Giveaway at ea0779809291c9708f1050ff87e666b3337f...
A multipurpose Discord Bot designed to carry out most of your server's needs with great performance and stability. - skyra-project/skyra
Sympact06
Sympact06โ€ข2mo ago
skyra uses a own lib too
Favna
Favnaโ€ข2mo ago
no? wdym?
Sympact06
Sympact06โ€ข2mo ago
import { LanguageKeys } from '#lib/i18n/languageKeys'; import { SkyraCommand } from '#lib/structures'; import type { GuildMessage } from '#lib/types'; import { Schedules } from '#lib/types/Enums';
Favna
Favnaโ€ข2mo ago
those are NodeJS subpath imports
Favna
Favnaโ€ข2mo ago
GitHub
skyra/src/lib/structures/commands/SkyraCommand.ts at ea0779809291c9...
A multipurpose Discord Bot designed to carry out most of your server's needs with great performance and stability. - skyra-project/skyra
Favna
Favnaโ€ข2mo ago
it is outdated code tho this is Sapphire v2
Sympact06
Sympact06โ€ข2mo ago
Ill just make my own simple giveaway command
Favna
Favnaโ€ข2mo ago
worse.. v1 I think
Sympact06
Sympact06โ€ข2mo ago
everything except the actual giveaway ending works
Favna
Favnaโ€ข2mo ago
oh no it is v2 but a very early stage of v2
Sympact06
Sympact06โ€ข2mo ago
can you use setInterval? for things like this
Favna
Favnaโ€ข2mo ago
you can but setInterval depends on process cycles so will desync if the giveaway runs for too long (i.e. a day or more)
Sympact06
Sympact06โ€ข2mo ago
whats a better way? node-cron?
Favna
Favnaโ€ข2mo ago
@sapphire/plugin-scheduled-tasks
Sympact06
Sympact06โ€ข2mo ago
damn
Favna
Favnaโ€ข2mo ago
which uses BullMQ which in turn uses Redis
Favna
Favnaโ€ข2mo ago
uh old link
Sympact06
Sympact06โ€ข2mo ago
found
Favna
Favnaโ€ข2mo ago
where did you get that? needs to be updated
Sympact06
Sympact06โ€ข2mo ago
google
Favna
Favnaโ€ข2mo ago
oh
Sympact06
Sympact06โ€ข2mo ago
this one works
Favna
Favnaโ€ข2mo ago
google you silly boy yeah I recently updated the website so it uses new doc gen stuff now the url is proper
Sympact06
Sympact06โ€ข2mo ago
but why redis etc I want to host 1 single thing and thats the bot xd
Favna
Favnaโ€ข2mo ago
as an example I use this plugin for @Dragonite to periodically dump stats to InfluxDB
Favna
Favnaโ€ข2mo ago
because it uses https://docs.bullmq.io
What is BullMQ | BullMQ
General description of BullMQ and its features
Sympact06
Sympact06โ€ข2mo ago
do I need to host another serve rthenm
Favna
Favnaโ€ข2mo ago
what do you host your bot on rn
Sympact06
Sympact06โ€ข2mo ago
pterodactyl and just localhost when dev
Favna
Favnaโ€ข2mo ago
no idea how that works then. Never used it
Sympact06
Sympact06โ€ข2mo ago
ill stick with nodecron then ig
Favna
Favnaโ€ข2mo ago
Pterodactylยฎ is a free, open-source game server management
This concerns me though something about fitting a square peg in a round hole by using it for a bot
Sympact06
Sympact06โ€ข2mo ago
its just a docker management system like portainer but normally designed for games indeed
Favna
Favnaโ€ข2mo ago
well you'd run Redis as a Docker container too but sounds like you're not really familiar with hosting infrastructure yet so take it slow and learn as you go btw you will need to host that database you use Prisma for somewhere ๐Ÿ‘€
Sympact06
Sympact06โ€ข2mo ago
I know So I got this from gpt as I dont know what the heck cron schedule is
import cron from 'node-cron';
import { PrismaClient } from '@prisma/client';
import { TextChannel } from 'discord.js';
const prisma = new PrismaClient();


class Scheduler {
public start() {
// Run every minute
cron.schedule('* * * * *', async () => {
const now = new Date();
const giveaways = await prisma.giveaway.findMany({
where: {
endsAt: {
lte: now,
},
},
});

for (const giveaway of giveaways) {
await this.endGiveaway(giveaway.id);
}
});
}

private async endGiveaway(giveawayId: number) {
const giveaway = await prisma.giveaway.findUnique({
where: { id: giveawayId },
});

if (!giveaway) return;

const channel = await client.channels.fetch(giveaway.channelId) as TextChannel;
if (!channel) return;

const message = await channel.messages.fetch(giveaway.messageId);
if (!message) return;

const users = await message.reactions.resolve('๐ŸŽ‰')?.users.fetch();
if (!users) return;

const participants = users.filter(user => !user.bot).map(user => user.id);
if (participants.length === 0) {
await channel.send(`No participants for the giveaway: ${giveaway.prize}`);
return;
}

const winners = this.pickWinners(participants, giveaway.winners);

await channel.send(`Congratulations to the winners of the giveaway for **${giveaway.prize}**: ${winners.map(id => `<@${id}>`).join(', ')}`);

await prisma.giveaway.delete({
where: { id: giveawayId },
});
}

private pickWinners(participants: string[], winnerCount: number): string[] {
const winners = new Set<string>();
while (winners.size < winnerCount && winners.size < participants.length) {
const randomIndex = Math.floor(Math.random() * participants.length);
winners.add(participants[randomIndex]);
}
return Array.from(winners);
}
}

const scheduler = new Scheduler();
export default scheduler;
import cron from 'node-cron';
import { PrismaClient } from '@prisma/client';
import { TextChannel } from 'discord.js';
const prisma = new PrismaClient();


class Scheduler {
public start() {
// Run every minute
cron.schedule('* * * * *', async () => {
const now = new Date();
const giveaways = await prisma.giveaway.findMany({
where: {
endsAt: {
lte: now,
},
},
});

for (const giveaway of giveaways) {
await this.endGiveaway(giveaway.id);
}
});
}

private async endGiveaway(giveawayId: number) {
const giveaway = await prisma.giveaway.findUnique({
where: { id: giveawayId },
});

if (!giveaway) return;

const channel = await client.channels.fetch(giveaway.channelId) as TextChannel;
if (!channel) return;

const message = await channel.messages.fetch(giveaway.messageId);
if (!message) return;

const users = await message.reactions.resolve('๐ŸŽ‰')?.users.fetch();
if (!users) return;

const participants = users.filter(user => !user.bot).map(user => user.id);
if (participants.length === 0) {
await channel.send(`No participants for the giveaway: ${giveaway.prize}`);
return;
}

const winners = this.pickWinners(participants, giveaway.winners);

await channel.send(`Congratulations to the winners of the giveaway for **${giveaway.prize}**: ${winners.map(id => `<@${id}>`).join(', ')}`);

await prisma.giveaway.delete({
where: { id: giveawayId },
});
}

private pickWinners(participants: string[], winnerCount: number): string[] {
const winners = new Set<string>();
while (winners.size < winnerCount && winners.size < participants.length) {
const randomIndex = Math.floor(Math.random() * participants.length);
winners.add(participants[randomIndex]);
}
return Array.from(winners);
}
}

const scheduler = new Scheduler();
export default scheduler;
but it couldnt find the client. Made it in src/Scheduler.ts
Favna
Favnaโ€ข2mo ago
https://crontab.guru is your friend
Crontab.guru - The cron schedule expression generator
An easy to use editor for crontab schedules.
Favna
Favnaโ€ข2mo ago
Sapphire Framework
Accessing the Client | Sapphire
Often when writing your [listeners], [commands], [preconditions], [arguments] and other pieces you will want to access