From 928170095002bc3196eea80f9fc0568edb24c59a Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 18:51:08 +0200 Subject: [PATCH 01/23] prisma was in dev deps, fix --- Dockerfile | 3 ++- package.json | 2 +- prisma.config.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index da308e1..f74b144 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,9 +16,10 @@ WORKDIR /app RUN corepack enable COPY package.json pnpm-lock.yaml ./ +COPY prisma.config.ts ./ COPY prisma ./prisma/ RUN pnpm install --prod --frozen-lockfile COPY --from=builder /app/dist ./dist -CMD ["sh", "-c", "pnpx prisma db push && pnpm run deploy && pnpm start"] +CMD ["sh", "-c", "pnpm prisma db push && pnpm run deploy && pnpm start"] diff --git a/package.json b/package.json index b91b630..32a5162 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/ms": "^2.1.0", "discord.js": "^14.24.0", "dotenv": "^17.4.2", + "prisma": "^7.7.0", "ms": "^2.1.3", "zod": "^4.3.6" }, @@ -30,7 +31,6 @@ "eslint-formatter-compact": "^8.40.0", "eslint-formatter-pretty": "^7.0.0", "prettier": "^3.6.2", - "prisma": "^7.7.0", "rimraf": "^6.1.3", "typescript": "~5.9.3" }, diff --git a/prisma.config.ts b/prisma.config.ts index 831a20f..e4a37aa 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -9,6 +9,6 @@ export default defineConfig({ path: "prisma/migrations", }, datasource: { - url: process.env["DATABASE_URL"], + url: env("DATABASE_URL"), }, }); From fc4865004eb86bd1ff320722dc241607bd26f10b Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 18:52:08 +0200 Subject: [PATCH 02/23] lockfile... --- pnpm-lock.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 584067d..b89327c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: ms: specifier: ^2.1.3 version: 2.1.3 + prisma: + specifier: ^7.7.0 + version: 7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) zod: specifier: ^4.3.6 version: 4.3.6 @@ -54,9 +57,6 @@ importers: prettier: specifier: ^3.6.2 version: 3.8.3 - prisma: - specifier: ^7.7.0 - version: 7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) rimraf: specifier: ^6.1.3 version: 6.1.3 From 3ad30affc97e024e0fc4542c364c44626d400acb Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 18:53:45 +0200 Subject: [PATCH 03/23] accidentally changed from process env to env: revert --- prisma.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma.config.ts b/prisma.config.ts index e4a37aa..bc0f80a 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -9,6 +9,6 @@ export default defineConfig({ path: "prisma/migrations", }, datasource: { - url: env("DATABASE_URL"), + url: process.env("DATABASE_URL"), }, }); From df00d9fec080eb915e4671ee0e9dd1635b37441f Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 18:55:00 +0200 Subject: [PATCH 04/23] FUC- --- prisma.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma.config.ts b/prisma.config.ts index bc0f80a..831a20f 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -9,6 +9,6 @@ export default defineConfig({ path: "prisma/migrations", }, datasource: { - url: process.env("DATABASE_URL"), + url: process.env["DATABASE_URL"], }, }); From fcf4a0a340218d218c3d0f79d910c37aa94e4645 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 19:16:14 +0200 Subject: [PATCH 05/23] patch: quick fix for mute/warn durations --- src/commands/moderation/mute.ts | 11 ++++++++--- src/commands/moderation/warn.ts | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index d5fcc11..7a67205 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -44,13 +44,18 @@ export default { const targetMember = interaction.options.getMember('target') as GuildMember const reason = interaction.options.getString('reason') ?? 'No reason provided'; const durationStr = interaction.options.getString('duration') as StringValue - const duration = ms(durationStr) ?? 'No duration provided' + const duration = durationStr ? ms(durationStr as StringValue) : null; const expiresAt = duration ? new Date(Date.now() + duration) : null; if (!targetMember) { await interaction.reply({ content: `${emojis.rightArrow2} That user is not in this server.`, flags: MessageFlags.Ephemeral }); return; } + + if (durationStr && (typeof duration !== 'number' || isNaN(duration))) { + await interaction.reply({ content: `${emojis.rightArrow2} Invalid duration format.`, flags: MessageFlags.Ephemeral }); + return; + } if (targetMember.id === interaction.user.id) { await interaction.reply({ content: `${emojis.rightArrow2} You cannot mute yourself.`, flags: MessageFlags.Ephemeral }); @@ -79,7 +84,7 @@ export default { const row = new ActionRowBuilder().addComponents(cancel, confirm); const response = await interaction.reply({ - content: `${emojis.rightArrow1} Are you sure you want to mute ${targetMember.user.username} for ${durationStr} with reason: ${reason}?`, + content: `${emojis.rightArrow1} Are you sure you want to mute ${targetMember.user.username} with reason: ${reason}?`, components: [row], withResponse: true, }); @@ -91,7 +96,7 @@ export default { if (confirmation.customId === 'confirm') { await createMute(interaction.guild.id, targetMember.id, expiresAt, reason); await enforceMute(interaction.guild, targetMember.id); - await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been muted for ${durationStr} with reason: ${reason}`, components: [] }); + await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been muted with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index b3e4a4e..f5a32f8 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -44,8 +44,13 @@ export default { const targetMember = interaction.options.getMember('target') as GuildMember const reason = interaction.options.getString('reason') ?? 'No reason provided'; const durationStr = interaction.options.getString('duration') as StringValue - const duration = durationStr ? ms(durationStr) : null + const duration = durationStr ? ms(durationStr as StringValue) : null; const expiresAt = duration ? new Date(Date.now() + duration) : null; + + if (durationStr && (typeof duration !== 'number' || isNaN(duration))) { + await interaction.reply({ content: `${emojis.rightArrow2} Invalid duration format.`, flags: MessageFlags.Ephemeral }); + return; + } if (!targetMember) { await interaction.reply({ content: `${emojis.rightArrow2} That user is not in this server.`, flags: MessageFlags.Ephemeral }); From d5ba5995e26a71096431e347914eedc390d3a06b Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 19:31:49 +0200 Subject: [PATCH 06/23] catch errors --- src/commands/moderation/ban.ts | 3 ++- src/commands/moderation/kick.ts | 3 ++- src/commands/moderation/mute.ts | 3 ++- src/commands/moderation/warn.ts | 7 ++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 630bfa7..6385943 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -83,7 +83,8 @@ export default { } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } - } catch { + } catch (err) { + console.error(err) await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] }); } }, diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index f20e572..5f682b2 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -87,7 +87,8 @@ export default { } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } - } catch { + } catch (err) { + console.error(err) await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] }); } }, diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 7a67205..fc70086 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -100,7 +100,8 @@ export default { } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } - } catch { + } catch (err) { + console.error(err) await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] }); } }, diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index f5a32f8..587fb27 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -84,7 +84,7 @@ export default { const row = new ActionRowBuilder().addComponents(cancel, confirm); const response = await interaction.reply({ - content: `${emojis.rightArrow1} Are you sure you want to warn ${targetMember.user.username} for ${durationStr} with reason: ${reason}?`, + content: `${emojis.rightArrow1} Are you sure you want to warn ${targetMember.user.username} with reason: ${reason}?`, components: [row], withResponse: true, }); @@ -95,11 +95,12 @@ export default { if (confirmation.customId === 'confirm') { await createWarn(interaction.guild.id, targetMember.id, interaction.user.id, reason, expiresAt); - await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been warned for ${durationStr} with reason: ${reason}`, components: [] }); + await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been warned with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } - } catch { + } catch(err) { + console.error(err) await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] }); } }, From 2d373ce34ce06819689af254cd5bde6e026f00dc Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 19:44:25 +0200 Subject: [PATCH 07/23] consistant schema + warn fix --- prisma/schema.prisma | 6 ++++++ src/commands/moderation/warn.ts | 2 +- src/lib/warns.ts | 12 ++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9ac5068..b78f4a2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,6 +16,8 @@ model Server { updatedAt DateTime @updatedAt warns Warn[] + mutes Mute[] + reminders Reminder[] @@map("server") } @@ -42,6 +44,8 @@ model Mute { reason String? expiresAt DateTime? createdAt DateTime @default(now()) + + server Server @relation(fields: [guildId], references: [id], onDelete: Cascade) @@unique([guildId, userId]) @@index([expiresAt]) @@ -55,6 +59,8 @@ model Reminder { message String remindAt DateTime createdAt DateTime @default(now()) + + server Server? @relation(fields: [guildId], references: [id], onDelete: Cascade) @@index([remindAt]) @@index([userId]) diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 587fb27..3e003a9 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -94,7 +94,7 @@ export default { const confirmation = await response.resource!.message!.awaitMessageComponent({ filter: collectorFilter, time: 60_000 }); if (confirmation.customId === 'confirm') { - await createWarn(interaction.guild.id, targetMember.id, interaction.user.id, reason, expiresAt); + await createWarn(interaction.guild.id, targetMember.id, interaction.user.id, interaction.guild.name, reason, expiresAt); await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been warned with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); diff --git a/src/lib/warns.ts b/src/lib/warns.ts index 8a20ed9..f729b73 100644 --- a/src/lib/warns.ts +++ b/src/lib/warns.ts @@ -2,13 +2,21 @@ import { prisma } from "./prisma.js"; export async function createWarn( guildId: string, + guildName: string, userId: string, moderatorId: string, reason: string, expiresAt: Date | null = null, ) { - return prisma.warn.create({ - data: { guildId, userId, moderatorId, reason, expiresAt }, + return prisma.$transaction(async (tx) => { + await tx.server.upsert({ + where: { id: guildId }, + create: { id: guildId, name: guildName }, + update: { name: guildName }, + }); + return tx.warn.create({ + data: { guildId, userId, moderatorId, reason, expiresAt }, + }); }); } From 98f66bf618f146285cce26913ad9f59b38d674ab Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 19:55:31 +0200 Subject: [PATCH 08/23] improve mute --- src/commands/moderation/mute.ts | 2 +- src/lib/mutes.ts | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index fc70086..e98fd16 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -94,7 +94,7 @@ export default { const confirmation = await response.resource!.message!.awaitMessageComponent({ filter: collectorFilter, time: 60_000 }); if (confirmation.customId === 'confirm') { - await createMute(interaction.guild.id, targetMember.id, expiresAt, reason); + await createMute(interaction.guild.id, targetMember.id, interaction.guild.name, expiresAt, reason); await enforceMute(interaction.guild, targetMember.id); await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been muted with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { diff --git a/src/lib/mutes.ts b/src/lib/mutes.ts index c8f886a..f963fbe 100644 --- a/src/lib/mutes.ts +++ b/src/lib/mutes.ts @@ -1,11 +1,24 @@ import { prisma } from "./prisma.js"; import { Guild } from 'discord.js'; -export async function createMute(guildId: string, userId: string, expiresAt: Date | null, reason?: string) { - return prisma.mute.upsert({ - where: { guildId_userId: { guildId, userId } }, - create: { guildId, userId, expiresAt, reason }, - update: { expiresAt, reason }, +export async function createMute( + guildId: string, + guildName: string, + userId: string, + expiresAt: Date | null, + reason?: string, +) { + return prisma.$transaction(async (tx) => { + await tx.server.upsert({ + where: { id: guildId }, + create: { id: guildId, name: guildName }, + update: { name: guildName }, + }); + return tx.mute.upsert({ + where: { guildId_userId: { guildId, userId } }, + create: { guildId, userId, expiresAt, reason }, + update: { expiresAt, reason }, + }); }); } From 0617eec7236b5bad4a462e6b150687bd94a7a5e7 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 20:14:04 +0200 Subject: [PATCH 09/23] General Improvements in formatting --- src/commands/moderation/ban.ts | 4 ++-- src/commands/moderation/kick.ts | 4 ++-- src/commands/moderation/mute.ts | 4 ++-- src/commands/moderation/unmute.ts | 4 ++-- src/commands/moderation/unwarn.ts | 2 +- src/commands/moderation/warn.ts | 4 ++-- src/commands/moderation/warns.ts | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 6385943..a5331a7 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -68,7 +68,7 @@ export default { const row = new ActionRowBuilder().addComponents(cancel, confirm); const response = await interaction.reply({ - content: `${emojis.rightArrow1} Are you sure you want to ban ${targetMember.user.username} for reason: ${reason}?`, + content: `${emojis.rightArrow1} Are you sure you want to ban <@${targetMember.user.username}> for reason: ${reason}?`, components: [row], withResponse: true, }); @@ -79,7 +79,7 @@ export default { if (confirmation.customId === 'confirm') { await interaction.guild!.members.ban(targetMember); - await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been banned with reason: ${reason}`, components: [] }); + await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.username}> has been banned with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 5f682b2..52e71d4 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -69,7 +69,7 @@ export default { const row = new ActionRowBuilder().addComponents(cancel, confirm); const response = await interaction.reply({ - content: `${emojis.rightArrow1} Are you sure you want to kick ${targetMember.user.username} with reason: ${reason}?`, + content: `${emojis.rightArrow1} Are you sure you want to kick <@${targetMember.user.id}> with reason: ${reason}?`, components: [row], withResponse: true, }); @@ -83,7 +83,7 @@ export default { `You have been kicked from **${interaction.guild.name}**.\nReason: ${reason}` ).catch(() => {}); await interaction.guild!.members.kick(targetMember); - await confirmation.update({ content: `${emojis.rightArrow2} **${targetMember.user.username}** has been kicked for reason: ${reason}\nYou must have had a real ick towards that person.`, components: [] }); + await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been kicked for reason: ${reason}\nYou must have had a real ick towards that person.`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index e98fd16..adc2bc8 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -84,7 +84,7 @@ export default { const row = new ActionRowBuilder().addComponents(cancel, confirm); const response = await interaction.reply({ - content: `${emojis.rightArrow1} Are you sure you want to mute ${targetMember.user.username} with reason: ${reason}?`, + content: `${emojis.rightArrow1} Are you sure you want to mute <@${targetMember.user.id}> with reason: ${reason}?`, components: [row], withResponse: true, }); @@ -96,7 +96,7 @@ export default { if (confirmation.customId === 'confirm') { await createMute(interaction.guild.id, targetMember.id, interaction.guild.name, expiresAt, reason); await enforceMute(interaction.guild, targetMember.id); - await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been muted with reason: ${reason}`, components: [] }); + await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been muted with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 0e96ea5..97591a9 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -60,7 +60,7 @@ export default { const row = new ActionRowBuilder().addComponents(cancel, confirm); const response = await interaction.reply({ - content: `${emojis.rightArrow1} Are you sure you want to unmute ${targetMember.user.username} with reason: ${reason}?`, + content: `${emojis.rightArrow1} Are you sure you want to unmute <@${targetMember.user.id}> with reason: ${reason}?`, components: [row], withResponse: true, }); @@ -72,7 +72,7 @@ export default { if (confirmation.customId === 'confirm') { await removeMute(interaction.guild.id, targetMember.id); targetMember.timeout(null, reason) - await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been unmuted with reason: ${reason}`, components: [] }); + await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been unmuted with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } diff --git a/src/commands/moderation/unwarn.ts b/src/commands/moderation/unwarn.ts index 8e90ce9..e16cb7f 100644 --- a/src/commands/moderation/unwarn.ts +++ b/src/commands/moderation/unwarn.ts @@ -57,7 +57,7 @@ export default { const row = new ActionRowBuilder().addComponents(cancel, confirm); const response = await interaction.reply({ - content: `${emojis.rightArrow1} Are you sure you want to unwarn ${warn.userId} with reason: ${reason}?\n${emojis.rightArrow2} They were warned for: ${warn.reason} `, + content: `${emojis.rightArrow1} Are you sure you want to unwarn <@${warn.userId}> with reason: ${reason}?\n${emojis.rightArrow2} They were warned for: ${warn.reason} `, components: [row], withResponse: true, }); diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 3e003a9..5f9ec58 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -84,7 +84,7 @@ export default { const row = new ActionRowBuilder().addComponents(cancel, confirm); const response = await interaction.reply({ - content: `${emojis.rightArrow1} Are you sure you want to warn ${targetMember.user.username} with reason: ${reason}?`, + content: `${emojis.rightArrow1} Are you sure you want to warn <@${targetMember.user.id}> with reason: ${reason}?`, components: [row], withResponse: true, }); @@ -95,7 +95,7 @@ export default { if (confirmation.customId === 'confirm') { await createWarn(interaction.guild.id, targetMember.id, interaction.user.id, interaction.guild.name, reason, expiresAt); - await confirmation.update({ content: `${emojis.rightArrow2} ${targetMember.user.username} has been warned with reason: ${reason}`, components: [] }); + await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been warned with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); } diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index 9b52355..a7c8f7a 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -36,7 +36,7 @@ export default { const active = await getActiveWarns(interaction.guild.id, targetMember.id); if (active.length === 0) { - await interaction.reply({ content: `${emojis.rightArrow2} ${targetMember.user.username} has no active warns.`, flags: MessageFlags.Ephemeral }); + await interaction.reply({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has no active warns.`, flags: MessageFlags.Ephemeral }); return; } @@ -47,7 +47,7 @@ export default { return `${emojis.rightArrow2} **${warn.reason ?? 'No reason provided'}** by <@${warn.moderatorId}>\n Expires: ${expires} \`${warn.id}\``; }); - let content = `${emojis.rightArrow1} ${active.length} active warn(s) for ${targetMember.user.username}:\n${lines.join('\n')}`; + let content = `${emojis.rightArrow1} ${active.length} active warn(s) for <@${targetMember.user.id}>:\n${lines.join('\n')}`; await interaction.reply({ content }); From b59c152b38674d95799ebadd89f16aa3b4f4ac1d Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 20:36:13 +0200 Subject: [PATCH 10/23] fix: positioning in functions inverted --- src/commands/moderation/warn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 5f9ec58..9dd04f7 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -94,7 +94,7 @@ export default { const confirmation = await response.resource!.message!.awaitMessageComponent({ filter: collectorFilter, time: 60_000 }); if (confirmation.customId === 'confirm') { - await createWarn(interaction.guild.id, targetMember.id, interaction.user.id, interaction.guild.name, reason, expiresAt); + await createWarn(interaction.guild.id, interaction.guild.name, targetMember.id, interaction.user.id, reason, expiresAt); await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been warned with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); From 0e1942c2dfb4c3f02fc8818d634c5d8742457e00 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 20:39:25 +0200 Subject: [PATCH 11/23] *also for mute --- src/commands/moderation/mute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index adc2bc8..6f28b0d 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -94,7 +94,7 @@ export default { const confirmation = await response.resource!.message!.awaitMessageComponent({ filter: collectorFilter, time: 60_000 }); if (confirmation.customId === 'confirm') { - await createMute(interaction.guild.id, targetMember.id, interaction.guild.name, expiresAt, reason); + await createMute(interaction.guild.id, interaction.guild.name, targetMember.id, expiresAt, reason); await enforceMute(interaction.guild, targetMember.id); await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been muted with reason: ${reason}`, components: [] }); } else if (confirmation.customId === 'cancel') { From 819a3e9b5f3f61b8b9f561aa0a17c95ce743eb42 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 20:49:24 +0200 Subject: [PATCH 12/23] Cleanup messages! --- src/commands/moderation/ban.ts | 8 ++++++++ src/commands/moderation/kick.ts | 8 ++++++++ src/commands/moderation/mute.ts | 9 +++++++++ src/commands/moderation/unmute.ts | 8 ++++++++ src/commands/moderation/unwarn.ts | 8 ++++++++ src/commands/moderation/warn.ts | 8 ++++++++ src/commands/moderation/warns.ts | 5 +++-- 7 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index a5331a7..7daee70 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -80,8 +80,16 @@ export default { if (confirmation.customId === 'confirm') { await interaction.guild!.members.ban(targetMember); await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.username}> has been banned with reason: ${reason}`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } } catch (err) { console.error(err) diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 52e71d4..49cdf62 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -84,8 +84,16 @@ export default { ).catch(() => {}); await interaction.guild!.members.kick(targetMember); await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been kicked for reason: ${reason}\nYou must have had a real ick towards that person.`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } } catch (err) { console.error(err) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 6f28b0d..2d8161c 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -97,8 +97,17 @@ export default { await createMute(interaction.guild.id, interaction.guild.name, targetMember.id, expiresAt, reason); await enforceMute(interaction.guild, targetMember.id); await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been muted with reason: ${reason}`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); + } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } } catch (err) { console.error(err) diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 97591a9..ef2cbfc 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -73,8 +73,16 @@ export default { await removeMute(interaction.guild.id, targetMember.id); targetMember.timeout(null, reason) await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been unmuted with reason: ${reason}`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } } catch { await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] }); diff --git a/src/commands/moderation/unwarn.ts b/src/commands/moderation/unwarn.ts index e16cb7f..33b0524 100644 --- a/src/commands/moderation/unwarn.ts +++ b/src/commands/moderation/unwarn.ts @@ -69,8 +69,16 @@ export default { if (confirmation.customId === 'confirm') { await removeWarn(warn.id); await confirmation.update({ content: `${emojis.rightArrow2} <@${warn.userId}> has been unwarned with reason: ${reason}`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } } catch { await interaction.editReply({ content: `${emojis.rightArrow2} No response within a minute or errored.`, components: [] }); diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 9dd04f7..05e73f4 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -96,8 +96,16 @@ export default { if (confirmation.customId === 'confirm') { await createWarn(interaction.guild.id, interaction.guild.name, targetMember.id, interaction.user.id, reason, expiresAt); await confirmation.update({ content: `${emojis.rightArrow2} <@${targetMember.user.id}> has been warned with reason: ${reason}`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } else if (confirmation.customId === 'cancel') { await confirmation.update({ content: `${emojis.rightArrow2} Cancelled.`, components: [] }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); } } catch(err) { console.error(err) diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index a7c8f7a..bd060be 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -51,7 +51,8 @@ export default { await interaction.reply({ content }); - - + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); }, } satisfies Command; From 1ea4bda52c5cbc6eb8636abdcd9ec416efd5f597 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 20:52:07 +0200 Subject: [PATCH 13/23] fix: warn must have logical pattern --- src/commands/moderation/mute.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 2d8161c..6d7be19 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -17,13 +17,13 @@ export default { }, { type: ApplicationCommandOptionType.String, - name: "reason", - description: "Provide a reason for their mute" + name: "duration", + description: "Specify a duration for the mute" }, { type: ApplicationCommandOptionType.String, - name: "duration", - description: "Specify a duration for the mute" + name: "reason", + description: "Provide a reason for their mute" } ] From 0094f1e7a444f36a27c5f7bf1adc39107dd0d0af Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 21:23:15 +0200 Subject: [PATCH 14/23] Heartbeat + improve warn list --- src/commands/moderation/warns.ts | 4 ---- src/events/ready.ts | 3 +++ src/util/heartbeat.ts | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 src/util/heartbeat.ts diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index bd060be..343b49c 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -50,9 +50,5 @@ export default { let content = `${emojis.rightArrow1} ${active.length} active warn(s) for <@${targetMember.user.id}>:\n${lines.join('\n')}`; await interaction.reply({ content }); - - setTimeout(() => { - interaction.deleteReply().catch(() => {}); - }, 3000); }, } satisfies Command; diff --git a/src/events/ready.ts b/src/events/ready.ts index 495e44f..020cc95 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,6 +1,7 @@ import { Events } from 'discord.js'; import type { Event } from './index.js'; import { enforceMute, getActiveMutes } from '#lib/mutes.js' +import { heartbeat } from '#utils/heartbeat.js'; export default { name: Events.ClientReady, @@ -8,6 +9,8 @@ export default { async execute(client) { console.log(`Ready! Logged in as ${client.user.tag}`); + heartbeat(client); + setInterval(async () => { const mutes = await getActiveMutes(); for (const mute of mutes) { diff --git a/src/util/heartbeat.ts b/src/util/heartbeat.ts new file mode 100644 index 0000000..7097763 --- /dev/null +++ b/src/util/heartbeat.ts @@ -0,0 +1,18 @@ +import type { Client } from 'discord.js'; + +export function heartbeat(client: Client) { + const pushURL = process.env['KUMA_PUSH_URL']; + if (!pushURL) return; + + const push = async () => { + if (!client.isReady()) return; + try { + await fetch(pushURL); + } catch (err) { + console.error("Push failed:", err); + } + }; + + push(); + setInterval(push, 60_000); +} From 98d87f5df67edb0038281a8f16811e8eadbd9e2f Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 22:25:11 +0200 Subject: [PATCH 15/23] ready: run on start + delete expired warns --- src/events/ready.ts | 10 ++++++++++ src/lib/warns.ts | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/src/events/ready.ts b/src/events/ready.ts index 020cc95..62a4ec7 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -2,6 +2,7 @@ import { Events } from 'discord.js'; import type { Event } from './index.js'; import { enforceMute, getActiveMutes } from '#lib/mutes.js' import { heartbeat } from '#utils/heartbeat.js'; +import { purgeExpiredWarns } from '#lib/warns.js'; export default { name: Events.ClientReady, @@ -11,7 +12,16 @@ export default { heartbeat(client); + purgeExpiredWarns().catch(err => console.error('[cleanup] purge failed:', err)); + + const mutes = await getActiveMutes(); + for (const mute of mutes) { + const guild = client.guilds.cache.get(mute.guildId); + if (guild) await enforceMute(guild, mute.userId); + } + setInterval(async () => { + await purgeExpiredWarns(); const mutes = await getActiveMutes(); for (const mute of mutes) { const guild = client.guilds.cache.get(mute.guildId); diff --git a/src/lib/warns.ts b/src/lib/warns.ts index f729b73..db42d6f 100644 --- a/src/lib/warns.ts +++ b/src/lib/warns.ts @@ -54,3 +54,12 @@ export async function getWarn(warnId: string, guildId: string) { if (!warn || warn.guildId !== guildId) return null; return warn; } + +export async function purgeExpiredWarns(gracePeriodDays = 90) { + const cutoff = new Date(Date.now() - gracePeriodDays * 24 * 60 * 60 * 1000); + return prisma.warn.deleteMany({ + where: { + expiresAt: { not: null, lte: cutoff }, + }, + }); +} From 32f5d73ca7569822598241bb3ae9cfaeb42c0bb4 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 23:38:03 +0200 Subject: [PATCH 16/23] chore: use github to build and send to caprover instead of caprover building --- .github/workflows/deploy.yaml | 56 ++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 1263ef9..d4b5464 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -9,6 +9,7 @@ on: permissions: contents: read + packages: write jobs: deploy: @@ -18,28 +19,41 @@ jobs: (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') steps: - - uses: actions/checkout@v6 + - name: Check out repository + uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_sha || github.sha }} - - uses: actions/setup-node@v6 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Preset Image Name + run: | + SHA="${{ github.event.workflow_run.head_sha || github.sha }}" + IMAGE=$(echo "ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:$(echo ${SHA} | cut -c1-7)" | tr '[:upper:]' '[:lower:]') + echo "IMAGE_URL=${IMAGE}" >> $GITHUB_ENV + + - name: Build and push Docker Image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ env.IMAGE_URL }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Deploy Image to CapRover + uses: caprover/deploy-from-github@v1.1.2 with: - node-version: '24' - - - name: Install CapRover CLI - run: npm install -g caprover - - - name: Create deploy tarball - run: tar -cvf deploy.tar --exclude='./.git' --exclude='./node_modules' --exclude='./dist' --exclude='./deploy.tar' . - - - name: Deploy - env: - CAPROVER_URL: ${{ secrets.CAPROVER_URL }} - CAPROVER_APP_TOKEN: ${{ secrets.CAPROVER_APP_TOKEN }} - CAPROVER_APP: ${{ secrets.CAPROVER_APP }} - run: > - caprover deploy - --caproverUrl "$CAPROVER_URL" - --appToken "$CAPROVER_APP_TOKEN" - --caproverApp "$CAPROVER_APP" - --tarFile ./deploy.tar + server: "${{ secrets.CAPROVER_URL }}" + app: "${{ secrets.CAPROVER_APP }}" + token: "${{ secrets.CAPROVER_APP_TOKEN }}" + image: ${{ env.IMAGE_URL }} From 0b8c48d617ef17f16bf0e50ad3f93e220afa38af Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Sun, 19 Apr 2026 23:42:27 +0200 Subject: [PATCH 17/23] chore: update versions --- .github/workflows/deploy.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index d4b5464..f37111e 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -20,15 +20,15 @@ jobs: github.event.workflow_run.head_branch == 'main') steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.workflow_run.head_sha || github.sha }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -41,7 +41,7 @@ jobs: echo "IMAGE_URL=${IMAGE}" >> $GITHUB_ENV - name: Build and push Docker Image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile From 4be5d3bcf18c9ea197f50dcfc5591fa36a579e95 Mon Sep 17 00:00:00 2001 From: UnbraveChimp <129645851+UnbraveChimp@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:29:57 +0200 Subject: [PATCH 18/23] Github: Add issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 28 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 21 +++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..1a685ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] ..." +labels: bug +assignees: '' +type: Bug + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..655cb54 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature] Add..." +labels: enhancement +assignees: '' +type: Feature + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 99aafca4235ad0b2955523c6e4e7ef4a9743d262 Mon Sep 17 00:00:00 2001 From: UnbraveChimp <129645851+UnbraveChimp@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:31:26 +0200 Subject: [PATCH 19/23] Github: Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1a685ac..e465a47 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,6 +11,8 @@ type: Bug **Describe the bug** A clear and concise description of what the bug is. +--- + **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' From 1654d30645ec3641bce0254c3b7af7e91bdfe796 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Mon, 20 Apr 2026 02:10:16 +0200 Subject: [PATCH 20/23] feat: reminders! --- CONTRIBUTING.md | 4 +- prisma/schema.prisma | 2 +- src/commands/utility/remind.ts | 119 +++++++++++++++++++++++++++++++++ src/events/ready.ts | 2 + src/lib/reminderEvent.ts | 40 +++++++++++ src/lib/reminders.ts | 51 ++++++++++++++ 6 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 src/commands/utility/remind.ts create mode 100644 src/lib/reminderEvent.ts create mode 100644 src/lib/reminders.ts diff --git a/ CONTRIBUTING.md b/ CONTRIBUTING.md index b4ee4d9..e09ac55 100644 --- a/ CONTRIBUTING.md +++ b/ CONTRIBUTING.md @@ -4,7 +4,7 @@ Contributing to Quest Bot requires basic knowledge of Typescript, Discord.JS and ## AI Rules -Please refrain from using AI to create new features. Using AI to help you understand something is fine but you can't have it vibecode or make features for you. This is especially important for HackClub events. +Please refrain from using AI to create new features. Using AI to help you *understand* how to make something is fine but you can't have it vibecode or make features for you. This is especially important for HackClub events. ## Pull requests @@ -18,6 +18,6 @@ Please write in the commit message what you have changed. ## Team -You will be invited to the Quest team if you have contributed a lot towards on or more Quest projects. +You will be invited to the Quest team if you have contributed a lot towards one or more Quest projects. ## Thanks! diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b78f4a2..9bd7698 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -53,8 +53,8 @@ model Mute { model Reminder { id String @id @default(cuid()) - userId String guildId String? + userId String channelId String? message String remindAt DateTime diff --git a/src/commands/utility/remind.ts b/src/commands/utility/remind.ts new file mode 100644 index 0000000..86c81d4 --- /dev/null +++ b/src/commands/utility/remind.ts @@ -0,0 +1,119 @@ +import type { Command } from '../index.js'; +import { ApplicationCommandOptionType, MessageFlags } from 'discord.js'; +import { emojis } from '#utils/emoji.js'; +import ms, { type StringValue } from 'ms'; +import { createReminder, getReminder, removeReminder } from '#lib/reminders.js'; + +export default { + data: { + name: 'remind', + description: 'Set reminders!', + options: [ + { + type: ApplicationCommandOptionType.Subcommand, + name: 'add', + description: 'Set a new reminder.', + options: [ + { + type: ApplicationCommandOptionType.String, + name: 'duration', + description: 'When to remind you', + required: true, + }, + { + type: ApplicationCommandOptionType.String, + name: 'message', + description: 'What to remind you about', + required: true, + }, + ], + }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'remove', + description: 'Cancel a reminder.', + options: [ + { + type: ApplicationCommandOptionType.String, + name: 'id', + description: 'The reminder ID', + required: true, + }, + ], + }, + ], + }, + async execute(interaction) { + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'add') { + + const durationStr = interaction.options.getString('duration', true); + const message = interaction.options.getString('message', true); + + const duration = ms(durationStr as StringValue); + if (typeof duration !== 'number' || isNaN(duration) || duration <= 0) { + await interaction.reply({ + content: `${emojis.rightArrow2} Invalid duration.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (duration > 31536000000 ) { + await interaction.reply({ + content: `${emojis.rightArrow2} Reminder cannot be longer than 1 year.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + const remindAt = new Date(Date.now() + duration); + + const reminder = interaction.inCachedGuild() + ? await createReminder( + interaction.user.id, + message, + remindAt, + interaction.guild.id, + interaction.guild.name, + interaction.channelId, + ) + : await createReminder(interaction.user.id, message, remindAt); + + const unix = Math.floor(remindAt.getTime() / 1000); + await interaction.reply({ + content: `${emojis.rightArrow2} Reminder message: ${message}\nID: \`${reminder.id}\``, + }); + return; + } + + if (subcommand === 'remove') { + const id = interaction.options.getString('id', true); + const reminder = await getReminder(id); + + if (!reminder) { + await interaction.reply({ + content: `${emojis.rightArrow2} No reminder found.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (reminder.userId !== interaction.user.id) { + await interaction.reply({ + content: `${emojis.rightArrow2} You can't remove other's reminders.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + await removeReminder(id); + await interaction.reply({ + content: `${emojis.rightArrow2} Reminder removed.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + }, +} satisfies Command; diff --git a/src/events/ready.ts b/src/events/ready.ts index 62a4ec7..a99d465 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -3,6 +3,7 @@ import type { Event } from './index.js'; import { enforceMute, getActiveMutes } from '#lib/mutes.js' import { heartbeat } from '#utils/heartbeat.js'; import { purgeExpiredWarns } from '#lib/warns.js'; +import { reminderScheduler } from '#lib/reminderEvent.js'; export default { name: Events.ClientReady, @@ -12,6 +13,7 @@ export default { heartbeat(client); + reminderScheduler(client); purgeExpiredWarns().catch(err => console.error('[cleanup] purge failed:', err)); const mutes = await getActiveMutes(); diff --git a/src/lib/reminderEvent.ts b/src/lib/reminderEvent.ts new file mode 100644 index 0000000..c6d0e59 --- /dev/null +++ b/src/lib/reminderEvent.ts @@ -0,0 +1,40 @@ +import type { Client } from 'discord.js'; +import { emojis } from '#utils/emoji.js'; +import { getDueReminders, removeReminder } from './reminders.js'; + +export function reminderScheduler(client: Client) { + const checkReminders = async () => { + try { + const due = await getDueReminders(); + + for (const reminder of due) { + if (reminder.channelId){ + const channel = await client.channels.fetch(reminder.channelId).catch(() => null); + + if (channel?.isSendable()) { + await channel.send({ + content: `${emojis.rightArrow2} <@${reminder.userId}> reminder: ${reminder.message}`,} + ); + } + } else { + await dmUser(client, reminder.userId, reminder.message); + } + + await removeReminder(reminder.id); + } + } catch (err) { + console.error(err) + } + } + checkReminders(); + setInterval(checkReminders, 30_000); +} + +async function dmUser(client: Client, userId: string, message: string ){ + const user = await client.users.fetch(userId).catch(() => null); + if (!user) return; + + await user.send({ + content: `${emojis.rightArrow2} <@${userId}> reminder: ${message}`, + }).catch(() => {}); +} diff --git a/src/lib/reminders.ts b/src/lib/reminders.ts new file mode 100644 index 0000000..5dc4b2d --- /dev/null +++ b/src/lib/reminders.ts @@ -0,0 +1,51 @@ +import { prisma } from "./prisma.js"; + +export async function createReminder( + userId: string, + message: string, + remindAt: Date, + guildId?: string, + guildName?: string, + channelId?: string, +) { + if (guildId && guildName) { + return prisma.$transaction(async (tx) => { + await tx.server.upsert({ + where: { id: guildId }, + create: { id: guildId, name: guildName }, + update: { name: guildName }, + }); + return tx.reminder.create({ + data: { guildId, userId, channelId, message, remindAt }, + }); + }); + } + return prisma.reminder.create({ + data: { userId, channelId, message, remindAt }, + }); +} + +export async function getReminders(guildId: string, userId: string) { + return prisma.reminder.findMany({ + where: { guildId, userId }, + orderBy: { remindAt: 'asc' }, + }); +} + +export async function removeReminder(reminderId: string) { + return prisma.reminder.delete({ where: { id: reminderId } }); +} + +export async function clearReminders(guildId: string, userId: string) { + return prisma.reminder.deleteMany({ where: { guildId, userId } }); +} + +export async function getReminder(reminderId: string) { + return prisma.reminder.findUnique({ where: { id: reminderId } }); +} + +export async function getDueReminders() { + return prisma.reminder.findMany({ + where: { remindAt: { lte: new Date() } }, + }); +} From 6aca30d6b3222d8dfcd6ddd7cca6aa79d097a265 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Mon, 20 Apr 2026 02:23:58 +0200 Subject: [PATCH 21/23] upd: improvements on reminders and auto-deletion --- src/commands/ping.ts | 4 +++ .../utility/{remind.ts => reminder.ts} | 12 +++++-- src/commands/utility/reminders.ts | 34 +++++++++++++++++++ src/commands/utility/user.ts | 4 +++ src/lib/reminders.ts | 4 +-- 5 files changed, 54 insertions(+), 4 deletions(-) rename src/commands/utility/{remind.ts => reminder.ts} (91%) create mode 100644 src/commands/utility/reminders.ts diff --git a/src/commands/ping.ts b/src/commands/ping.ts index d39f75c..5678a5d 100644 --- a/src/commands/ping.ts +++ b/src/commands/ping.ts @@ -14,5 +14,9 @@ export default { const ping = reply.createdTimestamp - interaction.createdTimestamp; interaction.editReply(`${emojis.rightArrow1} Client: ${ping}ms\n${emojis.rightArrow1} Websocket: ${interaction.client.ws.ping}ms`) + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); }, } satisfies Command; diff --git a/src/commands/utility/remind.ts b/src/commands/utility/reminder.ts similarity index 91% rename from src/commands/utility/remind.ts rename to src/commands/utility/reminder.ts index 86c81d4..2cedb06 100644 --- a/src/commands/utility/remind.ts +++ b/src/commands/utility/reminder.ts @@ -6,7 +6,7 @@ import { createReminder, getReminder, removeReminder } from '#lib/reminders.js'; export default { data: { - name: 'remind', + name: 'reminder', description: 'Set reminders!', options: [ { @@ -83,8 +83,12 @@ export default { const unix = Math.floor(remindAt.getTime() / 1000); await interaction.reply({ - content: `${emojis.rightArrow2} Reminder message: ${message}\nID: \`${reminder.id}\``, + content: `${emojis.rightArrow2} Reminder set to go off in message: ${message}\nID: \`${reminder.id}\``, }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); return; } @@ -113,6 +117,10 @@ export default { content: `${emojis.rightArrow2} Reminder removed.`, flags: MessageFlags.Ephemeral, }); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); return; } }, diff --git a/src/commands/utility/reminders.ts b/src/commands/utility/reminders.ts new file mode 100644 index 0000000..86f69c0 --- /dev/null +++ b/src/commands/utility/reminders.ts @@ -0,0 +1,34 @@ +import type { Command } from '../index.js'; +import { MessageFlags } from 'discord.js'; +import { emojis } from '#utils/emoji.js'; +import { getReminders } from '#lib/reminders.js'; + +export default { + data: { + name: 'reminders', + description: 'List your current reminders.', + }, + async execute(interaction) { + const reminders = interaction.inCachedGuild() + ? await getReminders(interaction.user.id, interaction.guild.id) + : await getReminders(interaction.user.id); + + if (reminders.length === 0) { + await interaction.reply({ + content: `${emojis.rightArrow2} You have no active reminders${interaction.inCachedGuild() ? ' in this server' : ''}.`, + flags: MessageFlags.Ephemeral, + }); + return; + } + + const lines = reminders.map((r) => { + const unix = Math.floor(r.remindAt.getTime() / 1000); + return ` | ${r.message}\n \`${r.id}\``; + }); + + await interaction.reply({ + content: `${emojis.rightArrow2} Your reminders:\n${lines.join('\n')}`, + flags: MessageFlags.Ephemeral, + }); + }, +} satisfies Command; diff --git a/src/commands/utility/user.ts b/src/commands/utility/user.ts index c0a746b..8ef82c7 100644 --- a/src/commands/utility/user.ts +++ b/src/commands/utility/user.ts @@ -8,5 +8,9 @@ export default { }, async execute(interaction) { await interaction.reply(`${emojis.rightArrow2} This command was run by **${interaction.user.username}**.`); + + setTimeout(() => { + interaction.deleteReply().catch(() => {}); + }, 3000); }, } satisfies Command; diff --git a/src/lib/reminders.ts b/src/lib/reminders.ts index 5dc4b2d..6724b44 100644 --- a/src/lib/reminders.ts +++ b/src/lib/reminders.ts @@ -25,9 +25,9 @@ export async function createReminder( }); } -export async function getReminders(guildId: string, userId: string) { +export async function getReminders( userId: string, guildId?: string) { return prisma.reminder.findMany({ - where: { guildId, userId }, + where: { userId, guildId }, orderBy: { remindAt: 'asc' }, }); } From 2fab3abf82379ae11f201f4785cc542dde9ff914 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Mon, 20 Apr 2026 02:29:42 +0200 Subject: [PATCH 22/23] upd: presence --- src/events/ready.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/events/ready.ts b/src/events/ready.ts index a99d465..b0d0333 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,4 +1,4 @@ -import { Events } from 'discord.js'; +import { ActivityType, Events } from 'discord.js'; import type { Event } from './index.js'; import { enforceMute, getActiveMutes } from '#lib/mutes.js' import { heartbeat } from '#utils/heartbeat.js'; @@ -16,6 +16,15 @@ export default { reminderScheduler(client); purgeExpiredWarns().catch(err => console.error('[cleanup] purge failed:', err)); + client.user.setPresence({ + activities: [{ + name: 'custom', + type: ActivityType.Custom, + state: 'Upd: /reminder!', + }], + status: 'online', + }); + const mutes = await getActiveMutes(); for (const mute of mutes) { const guild = client.guilds.cache.get(mute.guildId); From 62d4b3bdc4a3f85258fef0d97fb8838944137a09 Mon Sep 17 00:00:00 2001 From: lunarash0 Date: Mon, 20 Apr 2026 04:02:37 +0200 Subject: [PATCH 23/23] upd: increase reminder duration --- src/commands/utility/reminder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/utility/reminder.ts b/src/commands/utility/reminder.ts index 2cedb06..cfa1c72 100644 --- a/src/commands/utility/reminder.ts +++ b/src/commands/utility/reminder.ts @@ -88,7 +88,7 @@ export default { setTimeout(() => { interaction.deleteReply().catch(() => {}); - }, 3000); + }, 5000); return; } @@ -120,7 +120,7 @@ export default { setTimeout(() => { interaction.deleteReply().catch(() => {}); - }, 3000); + }, 5000); return; } },