deploy #1
@@ -13,7 +13,7 @@ import { completeVerification } from './modules/verification/handlers.js';
|
||||
import { closePoll } from './modules/utility/poll.js';
|
||||
import { sendReminder } from './modules/utility/reminders.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 { expireBirthdayRole, runBirthdayCheck } from './modules/birthdays/index.js';
|
||||
import { ensureStatsRecurringJobs, startStatsWorker } from './modules/stats/index.js';
|
||||
@@ -303,7 +303,17 @@ export function startWorkers(context: BotContext): Worker[] {
|
||||
giveawayQueueName,
|
||||
async (job) => {
|
||||
if (job.name === 'giveawayEnd') {
|
||||
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;
|
||||
}
|
||||
if (job.name === 'giveawayCreate') {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { giveawayCommands } from './commands.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 {
|
||||
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()
|
||||
.setTitle(t(locale, 'giveaway.embed.title'))
|
||||
.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') });
|
||||
} else if (giveaway.paused) {
|
||||
embed.setDescription(t(locale, 'giveaway.embed.paused'));
|
||||
embed.setFooter({
|
||||
text: tf(locale, 'giveaway.embed.endsAt', {
|
||||
time: `<t:${Math.floor(giveaway.endsAt.getTime() / 1000)}:R>`
|
||||
})
|
||||
});
|
||||
embed.setDescription(
|
||||
`${t(locale, 'giveaway.embed.paused')}\n\n${tf(locale, 'giveaway.embed.endsAt', {
|
||||
time: endsAtText
|
||||
})}`
|
||||
);
|
||||
} else {
|
||||
embed.setDescription(t(locale, 'giveaway.embed.active'));
|
||||
embed.setFooter({
|
||||
text: tf(locale, 'giveaway.embed.endsAt', {
|
||||
time: `<t:${Math.floor(giveaway.endsAt.getTime() / 1000)}:R>`
|
||||
})
|
||||
});
|
||||
embed.setDescription(
|
||||
`${t(locale, 'giveaway.embed.active')}\n\n${tf(locale, 'giveaway.embed.endsAt', {
|
||||
time: endsAtText
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
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 } });
|
||||
if (!giveaway || giveaway.endedAt) {
|
||||
return;
|
||||
if (!giveaway) {
|
||||
throw new GiveawayError('not_found');
|
||||
}
|
||||
if (giveaway.endedAt) {
|
||||
throw new GiveawayError('already_ended');
|
||||
}
|
||||
|
||||
await cancelGiveawayJob(giveawayId);
|
||||
@@ -338,6 +344,7 @@ export async function endGiveaway(context: BotContext, giveawayId: string): Prom
|
||||
if (winners.length > 0) {
|
||||
await dmWinners(context, updated, winners, locale);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function rerollGiveaway(
|
||||
|
||||
@@ -83,9 +83,18 @@ export function GiveawaysManager({ guildId, initialGiveaways }: GiveawaysManager
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
const body = (await response.json().catch(() => null)) as (GiveawayDashboard & { error?: string }) | null;
|
||||
if (!response.ok || !body) {
|
||||
toast.error(body?.error ?? t('common.saveError'));
|
||||
const body = (await response.json().catch(() => null)) as
|
||||
| (GiveawayDashboard & { error?: string })
|
||||
| { 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;
|
||||
}
|
||||
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}`);
|
||||
await existingJob?.remove();
|
||||
|
||||
await addJobAndAwait(
|
||||
const { confirmed } = await addJobAndAwait(
|
||||
getGiveawayQueue(),
|
||||
getGiveawayQueueEvents(),
|
||||
'giveawayEnd',
|
||||
{ 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 } });
|
||||
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> {
|
||||
|
||||
@@ -258,7 +258,20 @@
|
||||
- [ ] Stack an/aus: stapeln vs. nur aktuelle Belohnungsrolle
|
||||
- [ ] 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)
|
||||
|
||||
|
||||
@@ -395,6 +395,7 @@ const de: Dictionary = {
|
||||
'giveaway.list.entrants': 'Teilnehmer',
|
||||
'giveaway.dm.won': 'Glückwunsch! Du hast **{prize}** auf **{guild}** gewonnen!',
|
||||
'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.paused': 'Dieses Giveaway ist pausiert.',
|
||||
'giveaway.error.already_joined': 'Du nimmst bereits teil.',
|
||||
@@ -980,6 +981,7 @@ const en: Dictionary = {
|
||||
'giveaway.list.entrants': 'entrants',
|
||||
'giveaway.dm.won': 'Congratulations! You won **{prize}** on **{guild}**!',
|
||||
'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.paused': 'This giveaway is paused.',
|
||||
'giveaway.error.already_joined': 'You already entered.',
|
||||
|
||||
Reference in New Issue
Block a user