diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 559da2f..17cef68 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,6 +11,7 @@ enum OptionKey { TAG_PREFIX MAX_TAGS_PER_MESSAGE DAYS_TO_KEEP_TAGS + DAYS_TO_KEEP_USER_BOT_MESSAGES } // Options for the bot, stored in the database. diff --git a/src/common/events/index.ts b/src/common/events/index.ts index d3f25bd..390008c 100644 --- a/src/common/events/index.ts +++ b/src/common/events/index.ts @@ -5,6 +5,7 @@ import { readyEvent } from '@/features/ready/index.js'; import type { DiscordEvent } from './types.js'; import archiveChannels from '@/features/archive-channels/index.js'; import { tagReceivedEvent } from '@/features/tags/tag-received.js'; +import { reactionAddEvent } from '@/features/reactions/index.js'; export const events: DiscordEvent[] = [ readyEvent, @@ -13,4 +14,5 @@ export const events: DiscordEvent[] = [ interactionCreateEvent, archiveChannels, tagReceivedEvent, + reactionAddEvent, ].flat(); diff --git a/src/features/reactions/index.ts b/src/features/reactions/index.ts new file mode 100644 index 0000000..1742c9e --- /dev/null +++ b/src/features/reactions/index.ts @@ -0,0 +1,38 @@ +import { createEvent } from '@/common/events/create-event.js'; +import { + Events, + MessageReaction, + PartialMessageReaction, + PartialUser, + User, +} from 'discord.js'; +import { removeUserBotMessage } from './remove-user-bot-message.js'; + +export type ReactionAddEvent = { + reaction: MessageReaction | PartialMessageReaction; + user: User | PartialUser; +}; + +const handlers = [removeUserBotMessage]; + +export const reactionAddEvent = createEvent( + { + name: Events.MessageReactionAdd, + }, + async (reaction, user, details) => { + if (user.bot) { + return; + } + try { + if (reaction.partial) { + await reaction.fetch(); + } + } catch { + return; + } + + for (const handler of handlers) { + await handler(reaction, user, details); + } + } +); diff --git a/src/features/reactions/remove-user-bot-message.ts b/src/features/reactions/remove-user-bot-message.ts new file mode 100644 index 0000000..ccab885 --- /dev/null +++ b/src/features/reactions/remove-user-bot-message.ts @@ -0,0 +1,45 @@ +import type { ClientEvents, Events, GuildMember } from 'discord.js'; +import { UserBotMessagesService } from '@/services/user-bot-messages/user-bot-messages-service.js'; + +const DELETE_EMOJIS = ['🗑️', '❌']; + +export const removeUserBotMessage: ( + ...args: ClientEvents[Events.MessageReactionAdd] +) => Promise | void = async (reaction, user) => { + if ( + reaction.emoji.name === null || + !DELETE_EMOJIS.includes(reaction.emoji.name) + ) { + return; + } + + if (reaction.message.author?.id !== user.client.user.id) { + return; + } + + const guild = reaction.message.guild; + if (guild === null) { + return; + } + + let member: GuildMember; + try { + member = + guild.members.cache.get(user.id) ?? (await guild.members.fetch(user.id)); + } catch { + return; + } + + try { + const deleted = await UserBotMessagesService.deleteUserBotMessage({ + messageId: reaction.message.id, + user: member, + }); + + if (deleted) { + await reaction.message.delete(); + } + } catch { + return; + } +}; diff --git a/src/features/ready/index.ts b/src/features/ready/index.ts index 95c365c..93ac662 100644 --- a/src/features/ready/index.ts +++ b/src/features/ready/index.ts @@ -6,6 +6,7 @@ import { fetchAndCachePublicChannelsMessages } from '@/util/cache.js'; import { syncGuidesToChannel } from '@/util/post-guides.js'; import { leaveIfNotAllowedServer } from '@/util/server-guard.js'; import { syncArchiveCategoryChannels } from '../archive-channels/util.js'; +import { UserBotMessagesService } from '@/services/user-bot-messages/user-bot-messages-service.js'; export const readyEvent = createEvent( { @@ -71,5 +72,7 @@ export const readyEvent = createEvent( error ); } + + void UserBotMessagesService.startExpiredMessageCleanup(); } ); diff --git a/src/options.ts b/src/options.ts index e3adc1e..001f629 100644 --- a/src/options.ts +++ b/src/options.ts @@ -23,6 +23,11 @@ const OptionsDefaults = { type: 'number', displayName: 'Days to Keep Tags', }, + [OptionKey.DAYS_TO_KEEP_USER_BOT_MESSAGES]: { + value: '7', + type: 'number', + displayName: 'Days to Keep User Bot Messages', + }, } as const satisfies Record; export type OptionTypeOf = diff --git a/src/services/user-bot-messages/user-bot-messages-service.ts b/src/services/user-bot-messages/user-bot-messages-service.ts index dcca54a..3a6bb60 100644 --- a/src/services/user-bot-messages/user-bot-messages-service.ts +++ b/src/services/user-bot-messages/user-bot-messages-service.ts @@ -2,8 +2,8 @@ import type { GuildMember } from 'discord.js'; import { prisma } from '@/db/prisma.js'; import { isStaff } from '@/util/permissions.js'; import { DAY } from '@/constants/time.js'; - -const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // every hour +import { getBotOption } from '@/options.js'; +import { OptionKey } from '@/generated/prisma/enums.js'; export const UserBotMessagesService = { async deleteUserBotMessage({ @@ -65,6 +65,9 @@ export const UserBotMessagesService = { }; void cleanup(); - setInterval(cleanup, CLEANUP_INTERVAL_MS); + const daysToKeep = getBotOption( + OptionKey.DAYS_TO_KEEP_USER_BOT_MESSAGES + ).value; + setInterval(cleanup, daysToKeep * DAY); }, };