fix: Giveaway bugs
This commit is contained in:
@@ -13,7 +13,7 @@ import { completeVerification } from './modules/verification/handlers.js';
|
|||||||
import { closePoll } from './modules/utility/poll.js';
|
import { closePoll } from './modules/utility/poll.js';
|
||||||
import { sendReminder } from './modules/utility/reminders.js';
|
import { sendReminder } from './modules/utility/reminders.js';
|
||||||
import { expireSelfRole } from './modules/selfroles/service.js';
|
import { expireSelfRole } from './modules/selfroles/service.js';
|
||||||
import { endGiveaway, runGiveawayCreateJob } from './modules/giveaways/index.js';
|
import { endGiveaway, runGiveawayCreateJob, GiveawayError } from './modules/giveaways/index.js';
|
||||||
import { closeInactiveTickets } from './modules/tickets/index.js';
|
import { closeInactiveTickets } from './modules/tickets/index.js';
|
||||||
import { expireBirthdayRole, runBirthdayCheck } from './modules/birthdays/index.js';
|
import { expireBirthdayRole, runBirthdayCheck } from './modules/birthdays/index.js';
|
||||||
import { ensureStatsRecurringJobs, startStatsWorker } from './modules/stats/index.js';
|
import { ensureStatsRecurringJobs, startStatsWorker } from './modules/stats/index.js';
|
||||||
@@ -303,7 +303,17 @@ export function startWorkers(context: BotContext): Worker[] {
|
|||||||
giveawayQueueName,
|
giveawayQueueName,
|
||||||
async (job) => {
|
async (job) => {
|
||||||
if (job.name === 'giveawayEnd') {
|
if (job.name === 'giveawayEnd') {
|
||||||
await endGiveaway(context, (job.data as GiveawayEndJob).giveawayId);
|
try {
|
||||||
|
await endGiveaway(context, (job.data as GiveawayEndJob).giveawayId);
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof GiveawayError &&
|
||||||
|
(error.code === 'already_ended' || error.code === 'not_found')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (job.name === 'giveawayCreate') {
|
if (job.name === 'giveawayCreate') {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { giveawayCommands } from './commands.js';
|
export { giveawayCommands } from './commands.js';
|
||||||
export { isGiveawayButton, handleGiveawayButton } from './buttons.js';
|
export { isGiveawayButton, handleGiveawayButton } from './buttons.js';
|
||||||
export { endGiveaway, scheduleGiveawayEnd, runGiveawayCreateJob } from './service.js';
|
export { endGiveaway, scheduleGiveawayEnd, runGiveawayCreateJob, GiveawayError } from './service.js';
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ function requirementLines(locale: 'de' | 'en', giveaway: Giveaway): string[] {
|
|||||||
|
|
||||||
export function buildGiveawayEmbed(locale: 'de' | 'en', giveaway: Giveaway): EmbedBuilder {
|
export function buildGiveawayEmbed(locale: 'de' | 'en', giveaway: Giveaway): EmbedBuilder {
|
||||||
const ended = giveaway.endedAt !== null;
|
const ended = giveaway.endedAt !== null;
|
||||||
|
const endsAtUnix = Math.floor(giveaway.endsAt.getTime() / 1000);
|
||||||
|
// Discord does not parse <t:…> timestamps in embed footers — only in
|
||||||
|
// description/fields/content. Keep relative + absolute for clarity.
|
||||||
|
const endsAtText = `<t:${endsAtUnix}:R> (<t:${endsAtUnix}:f>)`;
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(t(locale, 'giveaway.embed.title'))
|
.setTitle(t(locale, 'giveaway.embed.title'))
|
||||||
.setColor(ended ? 0x6b7280 : 0x6366f1)
|
.setColor(ended ? 0x6b7280 : 0x6366f1)
|
||||||
@@ -86,19 +91,17 @@ export function buildGiveawayEmbed(locale: 'de' | 'en', giveaway: Giveaway): Emb
|
|||||||
);
|
);
|
||||||
embed.setFooter({ text: t(locale, 'giveaway.embed.ended') });
|
embed.setFooter({ text: t(locale, 'giveaway.embed.ended') });
|
||||||
} else if (giveaway.paused) {
|
} else if (giveaway.paused) {
|
||||||
embed.setDescription(t(locale, 'giveaway.embed.paused'));
|
embed.setDescription(
|
||||||
embed.setFooter({
|
`${t(locale, 'giveaway.embed.paused')}\n\n${tf(locale, 'giveaway.embed.endsAt', {
|
||||||
text: tf(locale, 'giveaway.embed.endsAt', {
|
time: endsAtText
|
||||||
time: `<t:${Math.floor(giveaway.endsAt.getTime() / 1000)}:R>`
|
})}`
|
||||||
})
|
);
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
embed.setDescription(t(locale, 'giveaway.embed.active'));
|
embed.setDescription(
|
||||||
embed.setFooter({
|
`${t(locale, 'giveaway.embed.active')}\n\n${tf(locale, 'giveaway.embed.endsAt', {
|
||||||
text: tf(locale, 'giveaway.embed.endsAt', {
|
time: endsAtText
|
||||||
time: `<t:${Math.floor(giveaway.endsAt.getTime() / 1000)}:R>`
|
})}`
|
||||||
})
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const requirements = requirementLines(locale, giveaway);
|
const requirements = requirementLines(locale, giveaway);
|
||||||
@@ -313,10 +316,13 @@ async function dmWinners(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function endGiveaway(context: BotContext, giveawayId: string): Promise<void> {
|
export async function endGiveaway(context: BotContext, giveawayId: string): Promise<Giveaway> {
|
||||||
const giveaway = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
|
const giveaway = await context.prisma.giveaway.findUnique({ where: { id: giveawayId } });
|
||||||
if (!giveaway || giveaway.endedAt) {
|
if (!giveaway) {
|
||||||
return;
|
throw new GiveawayError('not_found');
|
||||||
|
}
|
||||||
|
if (giveaway.endedAt) {
|
||||||
|
throw new GiveawayError('already_ended');
|
||||||
}
|
}
|
||||||
|
|
||||||
await cancelGiveawayJob(giveawayId);
|
await cancelGiveawayJob(giveawayId);
|
||||||
@@ -338,6 +344,7 @@ export async function endGiveaway(context: BotContext, giveawayId: string): Prom
|
|||||||
if (winners.length > 0) {
|
if (winners.length > 0) {
|
||||||
await dmWinners(context, updated, winners, locale);
|
await dmWinners(context, updated, winners, locale);
|
||||||
}
|
}
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rerollGiveaway(
|
export async function rerollGiveaway(
|
||||||
|
|||||||
@@ -83,9 +83,18 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ action })
|
body: JSON.stringify({ action })
|
||||||
});
|
});
|
||||||
const body = (await response.json().catch(() => null)) as (GiveawayDashboard & { error?: string }) | null;
|
const body = (await response.json().catch(() => null)) as
|
||||||
if (!response.ok || !body) {
|
| (GiveawayDashboard & { error?: string })
|
||||||
toast.error(body?.error ?? t('common.saveError'));
|
| { error?: string }
|
||||||
|
| null;
|
||||||
|
if (!response.ok || !body || !('id' in body)) {
|
||||||
|
toast.error(
|
||||||
|
(body && 'error' in body && body.error) || t('common.saveError')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'end' && !body.endedAt) {
|
||||||
|
toast.error(t('common.saveError'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setGiveaways((prev) => prev.map((entry) => (entry.id === giveaway.id ? body : entry)));
|
setGiveaways((prev) => prev.map((entry) => (entry.id === giveaway.id ? body : entry)));
|
||||||
|
|||||||
@@ -115,16 +115,24 @@ export async function endGiveawayDashboard(guildId: string, giveawayId: string):
|
|||||||
const existingJob = await getGiveawayQueue().getJob(`giveaway-end-${giveawayId}`);
|
const existingJob = await getGiveawayQueue().getJob(`giveaway-end-${giveawayId}`);
|
||||||
await existingJob?.remove();
|
await existingJob?.remove();
|
||||||
|
|
||||||
await addJobAndAwait(
|
const { confirmed } = await addJobAndAwait(
|
||||||
getGiveawayQueue(),
|
getGiveawayQueue(),
|
||||||
getGiveawayQueueEvents(),
|
getGiveawayQueueEvents(),
|
||||||
'giveawayEnd',
|
'giveawayEnd',
|
||||||
{ giveawayId },
|
{ giveawayId },
|
||||||
{ jobId: `giveaway-end-${giveawayId}`, removeOnComplete: true, removeOnFail: 50, delay: 0 }
|
{ jobId: `giveaway-end-${giveawayId}`, removeOnComplete: true, removeOnFail: 50, delay: 0 },
|
||||||
|
30_000
|
||||||
);
|
);
|
||||||
|
|
||||||
const updated = await prisma.giveaway.findUnique({ where: { id: giveawayId } });
|
const updated = await prisma.giveaway.findUnique({ where: { id: giveawayId } });
|
||||||
return updated ? toDashboard(updated) : null;
|
if (!updated?.endedAt) {
|
||||||
|
throw new Error(
|
||||||
|
confirmed
|
||||||
|
? 'Giveaway end job finished without marking the giveaway as ended'
|
||||||
|
: 'Giveaway end timed out – the bot may still be processing; refresh and retry'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return toDashboard(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteGiveawayDashboard(guildId: string, giveawayId: string): Promise<boolean> {
|
export async function deleteGiveawayDashboard(guildId: string, giveawayId: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -258,7 +258,20 @@
|
|||||||
- [ ] Stack an/aus: stapeln vs. nur aktuelle Belohnungsrolle
|
- [ ] Stack an/aus: stapeln vs. nur aktuelle Belohnungsrolle
|
||||||
- [ ] Doppeltes Level → Fehlermeldung, speichern blockiert
|
- [ ] Doppeltes Level → Fehlermeldung, speichern blockiert
|
||||||
|
|
||||||
## Post-Phase – Automod/Moderation Dashboard Ausbau (Status: implementiert)
|
## Post-Phase – Giveaway Bugs (Timestamp + End-Status) (Status: implementiert)
|
||||||
|
|
||||||
|
### Abgeschlossen (Code)
|
||||||
|
|
||||||
|
- Embed: Endzeit als Discord-Timestamp in der **Description** (Footer rendert `<t:…>` nicht)
|
||||||
|
- `/giveaway end` wirft bei fehlender/bereits beendeter ID Fehler statt Fake-Erfolg
|
||||||
|
- Dashboard „Jetzt beenden“ prüft `endedAt` nach Job; Timeout/Fehler → kein Erfolgs-Toast
|
||||||
|
|
||||||
|
### Manuell testen
|
||||||
|
|
||||||
|
- [ ] Neues Giveaway: Endzeit als „in X Stunden“ sichtbar, nicht als Roh-`<t:…>`
|
||||||
|
- [ ] `/giveaway end` mit falscher ID → Fehlermeldung; mit korrekter ID → Reroll möglich
|
||||||
|
- [ ] Dashboard „Jetzt beenden“ → Status beendet, Discord-Nachricht aktualisiert
|
||||||
|
|
||||||
|
|
||||||
### Abgeschlossen (Code)
|
### Abgeschlossen (Code)
|
||||||
|
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ const de: Dictionary = {
|
|||||||
'giveaway.list.entrants': 'Teilnehmer',
|
'giveaway.list.entrants': 'Teilnehmer',
|
||||||
'giveaway.dm.won': 'Glückwunsch! Du hast **{prize}** auf **{guild}** gewonnen!',
|
'giveaway.dm.won': 'Glückwunsch! Du hast **{prize}** auf **{guild}** gewonnen!',
|
||||||
'giveaway.error.not_found': 'Giveaway nicht gefunden oder bereits beendet.',
|
'giveaway.error.not_found': 'Giveaway nicht gefunden oder bereits beendet.',
|
||||||
|
'giveaway.error.already_ended': 'Dieses Giveaway ist bereits beendet.',
|
||||||
'giveaway.error.not_ended': 'Giveaway ist noch nicht beendet.',
|
'giveaway.error.not_ended': 'Giveaway ist noch nicht beendet.',
|
||||||
'giveaway.error.paused': 'Dieses Giveaway ist pausiert.',
|
'giveaway.error.paused': 'Dieses Giveaway ist pausiert.',
|
||||||
'giveaway.error.already_joined': 'Du nimmst bereits teil.',
|
'giveaway.error.already_joined': 'Du nimmst bereits teil.',
|
||||||
@@ -980,6 +981,7 @@ const en: Dictionary = {
|
|||||||
'giveaway.list.entrants': 'entrants',
|
'giveaway.list.entrants': 'entrants',
|
||||||
'giveaway.dm.won': 'Congratulations! You won **{prize}** on **{guild}**!',
|
'giveaway.dm.won': 'Congratulations! You won **{prize}** on **{guild}**!',
|
||||||
'giveaway.error.not_found': 'Giveaway not found or already ended.',
|
'giveaway.error.not_found': 'Giveaway not found or already ended.',
|
||||||
|
'giveaway.error.already_ended': 'This giveaway has already ended.',
|
||||||
'giveaway.error.not_ended': 'Giveaway has not ended yet.',
|
'giveaway.error.not_ended': 'Giveaway has not ended yet.',
|
||||||
'giveaway.error.paused': 'This giveaway is paused.',
|
'giveaway.error.paused': 'This giveaway is paused.',
|
||||||
'giveaway.error.already_joined': 'You already entered.',
|
'giveaway.error.already_joined': 'You already entered.',
|
||||||
|
|||||||
Reference in New Issue
Block a user