From 10ea38b6d53f85703cf01a8a36a80f7ffe9e26a4 Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:42:08 +0530 Subject: [PATCH 1/2] feat: dev alerts --- .vscode/command.code-snippets | 27 +++---- prisma/schema.prisma | 10 +++ src/commands/Main/inbox.ts | 90 ++++++++++++++++++++++++ src/commands/Staff/dev/index.ts | 18 +++++ src/commands/Staff/dev/send-alert.ts | 95 +++++++++++++++++++++++++ src/core/CommandContext/Context.ts | 12 ++-- src/events/interactionCreate.ts | 44 +++++++++--- src/events/messageCreate.ts | 52 +++++++++++--- src/modules/Pagination.ts | 101 +++++++++++++++------------ src/services/MessageProcessor.ts | 9 +-- src/services/UserDbService.ts | 1 + src/utils/Constants.ts | 1 + src/utils/Utils.ts | 23 +++++- 13 files changed, 385 insertions(+), 98 deletions(-) create mode 100644 src/commands/Main/inbox.ts create mode 100644 src/commands/Staff/dev/index.ts create mode 100644 src/commands/Staff/dev/send-alert.ts diff --git a/.vscode/command.code-snippets b/.vscode/command.code-snippets index 164f43af..fc26bb47 100644 --- a/.vscode/command.code-snippets +++ b/.vscode/command.code-snippets @@ -4,32 +4,21 @@ "prefix": ["command", "discord command"], "body": [ "import BaseCommand from '#src/core/BaseCommand.js';", - "import { ChatInputCommandInteraction } from 'discord.js';", + "import Context from '#src/core/CommandContext/Context.js';", "export default class $1 extends BaseCommand {", - "\treadonly data = {", - "\t\tname: '$2',", - "\t\tdescription: '$3',", - "\t};", - "\tasync execute(ctx: Context) {", - "\t\t$4", + "\tconstructor() {", + "\t\tsuper({", + "\t\t\tname: '$2',", + "\t\t\tdescription: '$3',", + "\t\t});", "\t}", - "}", - ], - "description": "Create a slash command with a name and description.", - }, - "Define an InterChat SubCommand": { - "scope": "javascript,typescript", - "prefix": ["subcommand", "discord subcommand"], - "body": [ - "import $1 from './index.js';", - "import { ChatInputCommandInteraction } from 'discord.js';\n", - "export default class $3 extends $1 {", + "\tasync execute(ctx: Context) {", "\t\t$4", "\t}", "}", ], - "description": "Create a slash subcommand with a name and description.", + "description": "Create a slash command with a name and description.", }, } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fb2b7376..3606f002 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -178,12 +178,22 @@ model UserData { ownedHubs Hub[] infractions Infraction[] @relation("infractions") issuedInfractions Infraction[] @relation("issuedInfractions") + inboxLastReadDate DateTime? @default(now()) @@index([xp]) @@index([level]) @@index([messageCount]) } +model Announcement { + id String @id @default(auto()) @map("_id") @db.ObjectId + title String + content String + thumbnailUrl String? + imageUrl String? + createdAt DateTime @default(now()) +} + model ServerData { id String @id @map("_id") @db.String premiumStatus Boolean @default(false) diff --git a/src/commands/Main/inbox.ts b/src/commands/Main/inbox.ts new file mode 100644 index 00000000..b8ef1bb3 --- /dev/null +++ b/src/commands/Main/inbox.ts @@ -0,0 +1,90 @@ +import BaseCommand from '#src/core/BaseCommand.js'; +import Context from '#src/core/CommandContext/Context.js'; +import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; +import { Pagination } from '#src/modules/Pagination.js'; +import UserDbService from '#src/services/UserDbService.js'; +import { CustomID } from '#src/utils/CustomID.js'; +import db from '#src/utils/Db.js'; +import { InfoEmbed } from '#src/utils/EmbedUtils.js'; +import { getEmoji } from '#src/utils/EmojiUtils.js'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, RepliableInteraction } from 'discord.js'; + +export default class InboxCommand extends BaseCommand { + private readonly userDbService = new UserDbService(); + + constructor() { + super({ + name: 'inbox', + description: 'Check your inbox for latest important updates & announcements', + types: { slash: true, prefix: true }, + }); + } + + async execute(ctx: Context) { + await showInbox(ctx, this.userDbService); + } + + @RegisterInteractionHandler('inbox', 'viewOlder') + async handleViewOlder(interaction: RepliableInteraction) { + await showInbox(interaction, this.userDbService, { showOlder: true, ephemeral: true }); + } +} + +export async function showInbox( + interaction: Context | RepliableInteraction, + userDbService: UserDbService, + opts?: { showOlder?: boolean; ephemeral?: boolean }, +) { + const userData = await userDbService.getUser(interaction.user.id); + const inboxLastRead = userData?.inboxLastReadDate || new Date(); + + const announcements = !opts?.showOlder + ? await db.announcement.findMany({ + where: { createdAt: { gt: inboxLastRead } }, + take: 10, + orderBy: { createdAt: 'desc' }, + }) + : await db.announcement.findMany({ + where: { createdAt: { lt: inboxLastRead } }, + take: 50, // limit to 50 older announcementsorderBy: { createdAt: 'desc' }, + }); + + const components = !opts?.showOlder + ? [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(new CustomID().setIdentifier('inbox', 'viewOlder').toString()) + .setLabel('View Older') + .setStyle(ButtonStyle.Secondary), + ), + ] + : []; + + if (announcements.length === 0) { + const embed = new InfoEmbed() + .setTitle(':tada: All caught up!') + .setDescription( + `I'll let you know when there's more. But for now, there's only Chipi here: ${getEmoji('chipi_smile', interaction.client)}`, + ); + await interaction.reply({ embeds: [embed], components }); + return; + } + + new Pagination(interaction.client, { hiddenButtons: ['search', 'select'] }) + .addPages( + announcements.map((announcement) => ({ + components, + embeds: [ + new InfoEmbed() + .setTitle(announcement.title) + .setDescription(announcement.content) + .setThumbnail(announcement.thumbnailUrl) + .setImage(announcement.imageUrl) + .setTimestamp(announcement.createdAt), + ], + })), + ) + .run(interaction, { ephemeral: opts?.ephemeral }); + + await userDbService.updateUser(interaction.user.id, { inboxLastReadDate: new Date() }); +} diff --git a/src/commands/Staff/dev/index.ts b/src/commands/Staff/dev/index.ts new file mode 100644 index 00000000..2f63862b --- /dev/null +++ b/src/commands/Staff/dev/index.ts @@ -0,0 +1,18 @@ +import DevAnnounceCommand from './send-alert.js'; +import BaseCommand from '#src/core/BaseCommand.js'; + +export default class DevCommand extends BaseCommand { + constructor() { + super({ + name: 'dev', + description: 'ooh spooky', + types: { + slash: true, + prefix: true, + }, + subcommands: { + 'send-alert': new DevAnnounceCommand(), + }, + }); + } +} diff --git a/src/commands/Staff/dev/send-alert.ts b/src/commands/Staff/dev/send-alert.ts new file mode 100644 index 00000000..78330d41 --- /dev/null +++ b/src/commands/Staff/dev/send-alert.ts @@ -0,0 +1,95 @@ +import BaseCommand from '#src/core/BaseCommand.js'; +import Context from '#src/core/CommandContext/Context.js'; +import { RegisterInteractionHandler } from '#src/decorators/RegisterInteractionHandler.js'; +import Constants from '#src/utils/Constants.js'; +import { CustomID } from '#src/utils/CustomID.js'; +import db from '#src/utils/Db.js'; +import { getEmoji } from '#src/utils/EmojiUtils.js'; +import { + ActionRowBuilder, + ModalBuilder, + ModalSubmitInteraction, + TextInputBuilder, + TextInputStyle, +} from 'discord.js'; + +export default class DevAnnounceCommand extends BaseCommand { + constructor() { + super({ + name: 'send-alert', + description: 'Alert something to all users. This will go to their inbox.', + types: { slash: true, prefix: true }, + }); + } + async execute(ctx: Context) { + const modal = new ModalBuilder() + .setCustomId(new CustomID('devAnnounceModal').toString()) + .setTitle('Announcement Creation') + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('title') + .setLabel('Title') + .setMaxLength(100) + .setRequired(true) + .setStyle(TextInputStyle.Short), + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('content') + .setLabel('Content of the announcement') + .setMaxLength(4000) + .setRequired(true) + .setStyle(TextInputStyle.Paragraph), + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('thumbnailUrl') + .setLabel('Thumbnail URL') + .setRequired(false) + .setStyle(TextInputStyle.Short), + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('bannerUrl') + .setLabel('Banner URL') + .setRequired(false) + .setStyle(TextInputStyle.Short), + ), + ); + + await ctx.showModal(modal); + } + + @RegisterInteractionHandler('devAnnounceModal') + async handleModal(interaction: ModalSubmitInteraction) { + const title = interaction.fields.getTextInputValue('title'); + const content = interaction.fields.getTextInputValue('content'); + const thumbnailUrlInput = interaction.fields.getTextInputValue('thumbnailUrl'); + const imageUrlInput = interaction.fields.getTextInputValue('bannerUrl'); + + const thumbnailUrl = thumbnailUrlInput.length > 0 ? thumbnailUrlInput : null; + const imageUrl = imageUrlInput.length > 0 ? imageUrlInput : null; + + const testThumbnail = + thumbnailUrlInput.length > 0 ? Constants.Regex.ImageURL.test(thumbnailUrlInput) : true; + const testImage = + imageUrlInput.length > 0 ? Constants.Regex.ImageURL.test(imageUrlInput) : true; + + if (!testThumbnail || !testImage) { + await interaction.reply({ + content: `${getEmoji('x_icon', interaction.client)} Thumbnail or Icon URL is invalid.`, + flags: ['Ephemeral'], + }); + return; + } + + await db.announcement.create({ + data: { title, content, thumbnailUrl, imageUrl }, + }); + + await interaction.reply( + `${getEmoji('tick_icon', interaction.client)} Announcement has been recorded. View using \`/inbox\``, + ); + } +} diff --git a/src/core/CommandContext/Context.ts b/src/core/CommandContext/Context.ts index 1b5e1aa6..9ce0b3fc 100644 --- a/src/core/CommandContext/Context.ts +++ b/src/core/CommandContext/Context.ts @@ -139,17 +139,15 @@ export default abstract class Context { if (this.interaction instanceof MessageContextMenuCommandInteraction) { return this.interaction.targetId; } - if (!name) return null; - - const value = this.options.getString(name); - if (!value) return null; - let messageId: string | null | undefined = extractMessageId(value); if (this.interaction instanceof Message && this.interaction.reference) { - messageId = this.interaction.reference.messageId; + return this.interaction.reference.messageId ?? null; } + if (!name) return null; - return messageId ?? null; + const value = this.options.getString(name); + if (!value) return null; + return extractMessageId(value) ?? null; } public async getTargetUser(name?: string) { diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 9ec7d5a4..f566105b 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -15,6 +15,22 @@ * along with InterChat. If not, see . */ +import type BaseCommand from '#src/core/BaseCommand.js'; +import BaseEventListener from '#src/core/BaseEventListener.js'; +import { showRulesScreening } from '#src/interactions/RulesScreening.js'; +import { executeCommand, resolveCommand } from '#src/utils/CommandUtils.js'; +import Constants from '#utils/Constants.js'; +import { CustomID, type ParsedCustomId } from '#utils/CustomID.js'; +import { InfoEmbed } from '#utils/EmbedUtils.js'; +import { t } from '#utils/Locale.js'; +import { + checkIfStaff, + createUnreadDevAlertEmbed, + fetchUserData, + fetchUserLocale, + handleError, + hasUnreadDevAlert, +} from '#utils/Utils.js'; import type { UserData } from '@prisma/client'; import type { AutocompleteInteraction, @@ -25,15 +41,6 @@ import type { MessageComponentInteraction, ModalSubmitInteraction, } from 'discord.js'; -import type BaseCommand from '#src/core/BaseCommand.js'; -import BaseEventListener from '#src/core/BaseEventListener.js'; -import { showRulesScreening } from '#src/interactions/RulesScreening.js'; -import Constants from '#utils/Constants.js'; -import { CustomID, type ParsedCustomId } from '#utils/CustomID.js'; -import { InfoEmbed } from '#utils/EmbedUtils.js'; -import { t } from '#utils/Locale.js'; -import { checkIfStaff, fetchUserData, fetchUserLocale, handleError } from '#utils/Utils.js'; -import { executeCommand, resolveCommand } from '#src/utils/CommandUtils.js'; export default class InteractionCreate extends BaseEventListener<'interactionCreate'> { readonly name = 'interactionCreate'; @@ -42,14 +49,31 @@ export default class InteractionCreate extends BaseEventListener<'interactionCre try { const preCheckResult = await this.performPreChecks(interaction); if (!preCheckResult.shouldContinue) return; + await this.handleInteraction(interaction, preCheckResult.dbUser).catch((e) => { + handleError(e, { repliable: interaction }); + }); - await this.handleInteraction(interaction, preCheckResult.dbUser); + await this.showDevAlertIfAny(interaction, preCheckResult.dbUser); } catch (e) { handleError(e, { repliable: interaction }); } } + private async showDevAlertIfAny(interaction: Interaction, dbUser: UserData | null) { + if (!interaction.isRepliable() || !interaction.replied || !dbUser) return; + + const shouldShow = await hasUnreadDevAlert(dbUser); + if (!shouldShow) return; + + await interaction + .followUp({ + embeds: [createUnreadDevAlertEmbed(this.getEmoji('info_icon'))], + flags: ['Ephemeral'], + }) + .catch(() => null); + } + private async performPreChecks(interaction: Interaction) { if (this.isInMaintenance(interaction)) { return { shouldContinue: false, dbUser: null }; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index d8f9fb27..60b9a3fe 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -15,23 +15,27 @@ * along with InterChat. If not, see . */ -import { stripIndents } from 'common-tags'; -import type { Client, Message } from 'discord.js'; import BaseEventListener from '#src/core/BaseEventListener.js'; import { showRulesScreening } from '#src/interactions/RulesScreening.js'; import { MessageProcessor } from '#src/services/MessageProcessor.js'; -import Constants from '#src/utils/Constants.js'; -import { fetchUserData, handleError, isHumanMessage } from '#utils/Utils.js'; +import UserDbService from '#src/services/UserDbService.js'; import { executeCommand, resolveCommand } from '#src/utils/CommandUtils.js'; +import Constants, { RedisKeys } from '#src/utils/Constants.js'; +import getRedis from '#src/utils/Redis.js'; +import { + createUnreadDevAlertEmbed, + fetchUserData, + handleError, + hasUnreadDevAlert, + isHumanMessage, +} from '#utils/Utils.js'; +import { stripIndents } from 'common-tags'; +import type { Message } from 'discord.js'; export default class MessageCreate extends BaseEventListener<'messageCreate'> { readonly name = 'messageCreate'; - private readonly messageProcessor: MessageProcessor; - - constructor(client: Client | null) { - super(client ?? null); - this.messageProcessor = new MessageProcessor(); - } + private readonly messageProcessor = new MessageProcessor(); + private readonly userDbService = new UserDbService(); async execute(message: Message) { if (!message.inGuild() || !isHumanMessage(message)) return; @@ -71,9 +75,35 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { if (!command) return; await executeCommand(message, command, prefixArgs); + await this.showDevAlertsIfAny(message); } private async handleChatMessage(message: Message) { - await this.messageProcessor.processHubMessage(message); + const { handled } = await this.messageProcessor.processHubMessage(message); + if (handled === true) this.showDevAlertsIfAny(message); + } + + private async showDevAlertsIfAny(message: Message) { + const redis = getRedis(); + const key = `${RedisKeys.DevAnnouncement}:${message.author.id}:lastAskedDate`; + const lastAsked = await redis.get(key); + + // check if the user has been asked to check inbox in the last 10 minutes + if (lastAsked && Date.now() - Number(lastAsked) < 600_000) return; + + const userData = await fetchUserData(message.author.id); + if (!userData) return; + + const shouldShow = await hasUnreadDevAlert(userData); + if (!shouldShow) return; + + await message + .reply({ + embeds: [createUnreadDevAlertEmbed(this.getEmoji('info_icon'))], + allowedMentions: { repliedUser: true }, + }) + .catch(() => null); + + await redis.set(key, Date.now().toString()); } } diff --git a/src/modules/Pagination.ts b/src/modules/Pagination.ts index 40a2897c..d2d5d6d8 100644 --- a/src/modules/Pagination.ts +++ b/src/modules/Pagination.ts @@ -28,7 +28,9 @@ import { type Client, ComponentType, type EmbedBuilder, + InteractionCallbackResponse, type InteractionReplyOptions, + InteractionResponse, Message, ModalBuilder, type ModalSubmitInteraction, @@ -45,7 +47,7 @@ type RunOptions = { idle?: number; ephemeral?: boolean; deleteOnEnd?: boolean }; export class Pagination { private readonly pages: BaseMessageOptions[] = []; - private readonly hiddenButtons: Partial> = {}; + private readonly hiddenButtons: (keyof ButtonEmojis)[] = []; private readonly client: Client; private customEmojis: ButtonEmojis; @@ -53,10 +55,10 @@ export class Pagination { client: Client, opts: { customEmojis?: ButtonEmojis; - hideButtons?: Partial>; + hiddenButtons?: (keyof ButtonEmojis)[]; } = {}, ) { - if (opts.hideButtons) this.hiddenButtons = opts.hideButtons; + if (opts.hiddenButtons) this.hiddenButtons = opts.hiddenButtons; this.client = client; this.customEmojis = opts.customEmojis ?? { @@ -240,7 +242,7 @@ export class Pagination { } } - public async run(ctx: PaginationInteraction | Message | Context, options?: RunOptions) { + public async run(ctx: RepliableInteraction | Message | Context, options?: RunOptions) { if (this.pages.length < 1) { await this.sendReply( ctx, @@ -260,6 +262,8 @@ export class Pagination { { flags: options?.ephemeral ? ['Ephemeral'] : [] }, ); + if (!listMessage) return; + const col = listMessage.createMessageComponentCollector({ idle: options?.idle || 60000, componentType: ComponentType.Button, @@ -301,10 +305,11 @@ export class Pagination { if (!interaction) { if (options?.ephemeral) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - options?.deleteOnEnd - ? await listMessage.delete() - : await listMessage.edit({ components: [] }); + + (options?.deleteOnEnd + ? listMessage.delete() + : listMessage.edit({ components: [] }) + ).catch(() => null); } return; } @@ -319,20 +324,28 @@ export class Pagination { await interaction.deleteReply(); } else if (ackd === false) { - await interaction.message.edit({ components: [] }).catch(() => null); + await interaction.message.edit({ components: [] }); } }); } private async sendReply( - ctx: PaginationInteraction | Message | Context, + ctx: RepliableInteraction | Message | Context, opts: BaseMessageOptions, interactionOpts?: { flags?: InteractionReplyOptions['flags'] }, - ) { + ): Promise { if (ctx instanceof Message || ctx instanceof Context) return await ctx.reply(opts); const replyMethod = getReplyMethod(ctx); - return await ctx[replyMethod]({ ...opts, flags: interactionOpts?.flags }); + const reply = await ctx[replyMethod]({ + ...opts, + flags: interactionOpts?.flags, + withResponse: true, + }); + + return 'resource' in reply + ? ((reply as unknown as InteractionCallbackResponse).resource?.message ?? null) + : reply; } private adjustIndex(customId: string, index: number) { @@ -349,42 +362,38 @@ export class Pagination { } private createButtons(index: number, totalPages: number) { - const { back, next, search, select } = this.hiddenButtons; + const buttons = { + select: new ButtonBuilder() + .setEmoji(this.customEmojis.select) + .setCustomId('page_:select') + .setStyle(ButtonStyle.Secondary), + back: new ButtonBuilder() + .setEmoji(this.customEmojis.back) + .setCustomId('page_:back') + .setStyle(ButtonStyle.Secondary) + .setDisabled(index === 0), + index: new ButtonBuilder() + .setCustomId('page_:index') + .setDisabled(true) + .setLabel(`${index + 1}/${totalPages}`) + .setStyle(ButtonStyle.Primary), + next: new ButtonBuilder() + .setEmoji(this.customEmojis.next) + .setCustomId('page_:next') + .setStyle(ButtonStyle.Secondary) + .setDisabled(totalPages <= index + 1), + search: new ButtonBuilder() + .setEmoji(this.customEmojis.search) + .setCustomId('page_:search') + .setStyle(ButtonStyle.Secondary), + }; return new ActionRowBuilder().addComponents( - [ - select - ? null - : new ButtonBuilder() - .setEmoji(this.customEmojis.select) - .setCustomId('page_:select') - .setStyle(ButtonStyle.Secondary), - back - ? null - : new ButtonBuilder() - .setEmoji(this.customEmojis.back) - .setCustomId('page_:back') - .setStyle(ButtonStyle.Secondary) - .setDisabled(index === 0), - new ButtonBuilder() - .setCustomId('page_:index') - .setDisabled(true) - .setLabel(`${index + 1}/${totalPages}`) - .setStyle(ButtonStyle.Primary), - next - ? null - : new ButtonBuilder() - .setEmoji(this.customEmojis.next) - .setCustomId('page_:next') - .setStyle(ButtonStyle.Secondary) - .setDisabled(totalPages <= index + 1), - search - ? null - : new ButtonBuilder() - .setEmoji(this.customEmojis.search) - .setCustomId('page_:search') - .setStyle(ButtonStyle.Secondary), - ].filter(Boolean) as ButtonBuilder[], + Object.entries(buttons) + .filter( + ([key]) => key === 'index' || !this.hiddenButtons.includes(key as keyof ButtonEmojis), + ) + .map(([, btn]) => btn), ); } } diff --git a/src/services/MessageProcessor.ts b/src/services/MessageProcessor.ts index 053031be..1674c9ce 100644 --- a/src/services/MessageProcessor.ts +++ b/src/services/MessageProcessor.ts @@ -44,16 +44,16 @@ export class MessageProcessor { return { hub, hubConnections, connection }; } - async processHubMessage(message: Message) { + async processHubMessage(message: Message): Promise<{ handled: boolean }> { const hubData = await MessageProcessor.getHubAndConnections(message.channelId, this.hubService); - if (!hubData) return; + if (!hubData) return { handled: false }; const { hub, hubConnections, connection } = hubData; const userData = await fetchUserData(message.author.id); if (!userData?.acceptedRules) { await showRulesScreening(message, userData); - return; + return { handled: true }; } const attachmentURL = await this.broadcastService.resolveAttachmentURL(message); @@ -66,7 +66,7 @@ export class MessageProcessor { totalHubConnections: hubConnections.length + 1, })) ) { - return; + return { handled: false }; } message.channel.sendTyping().catch(() => null); @@ -80,5 +80,6 @@ export class MessageProcessor { ); await message.client.userLevels.handleMessage(message); + return { handled: true }; } } diff --git a/src/services/UserDbService.ts b/src/services/UserDbService.ts index 80cc774c..19d712f9 100644 --- a/src/services/UserDbService.ts +++ b/src/services/UserDbService.ts @@ -36,6 +36,7 @@ export default class UserDbService { lastMessageAt: new Date(user.lastMessageAt), updatedAt: new Date(user.updatedAt), lastVoted: user.lastVoted ? new Date(user.lastVoted) : null, + inboxLastReadDate: new Date(user.inboxLastReadDate ?? 0), }; return { ...user, ...dates }; } diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index f218e366..170b9285 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -21,6 +21,7 @@ export const enum RedisKeys { messageReverse = 'messageReverse', Hub = 'hub', Spam = 'spam', + DevAnnouncement = 'DevAnnouncement', } export const enum ConnectionMode { diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 6d7e8b9c..ed04e219 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -16,6 +16,7 @@ */ import UserDbService from '#src/services/UserDbService.js'; +import db from '#src/utils/Db.js'; import { type ErrorHandlerOptions, createErrorHint, @@ -31,6 +32,7 @@ import { captureException } from '@sentry/node'; import { type CommandInteraction, type ContextMenuCommandInteraction, + EmbedBuilder, type Guild, type GuildTextBasedChannel, Message, @@ -252,7 +254,6 @@ export const extractRoleId = (input: string | undefined) => { export const extractMessageId = (input: string) => input.match(Constants.Regex.MessageId)?.[1] ?? null; - interface InviteCreationResult { success: boolean; inviteUrl?: string; @@ -275,3 +276,23 @@ export const createServerInvite = async ( inviteUrl: invite?.url, }; }; + +export const getLatestDevAlert = async () => + await db.announcement.findFirst({ orderBy: { createdAt: 'desc' } }); + +export const hasUnreadDevAlert = async (userData: UserData) => { + const latestDevAnnouncement = await getLatestDevAlert(); + return Boolean( + userData?.inboxLastReadDate && + latestDevAnnouncement && + latestDevAnnouncement.createdAt > userData.inboxLastReadDate, + ); +}; + +export const createUnreadDevAlertEmbed = (emoji: string) => + new EmbedBuilder() + .setTitle(`${emoji} You have a new message from the developers!`) + .setColor(Constants.Colors.invisible) + .setDescription( + 'Use `/inbox` or `c!inbox` to read the latest announcement and dismiss this message.', + ); From 4ee638e8f3ee058460164afdce81d779c85308a4 Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:56:52 +0530 Subject: [PATCH 2/2] fix(redis): set expiration time for lastAskedDate --- src/events/messageCreate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 60b9a3fe..22aac81e 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -104,6 +104,6 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { }) .catch(() => null); - await redis.set(key, Date.now().toString()); + await redis.set(key, Date.now().toString(), 'EX', 600); } }