diff --git a/package-lock.json b/package-lock.json index 9597f66..58762cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "discordbot-ng", "dependencies": { "@clynamic/dmark": "0.0.11", "@redis/client": "5.1.0", diff --git a/package.json b/package.json index b419e17..8a3c4d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "discordbot-ng", - "devDependencies": { "@eslint/js": "9.39.5", "@stylistic/eslint-plugin": "5.2.1", @@ -34,5 +33,9 @@ "copyfiles": "copyfiles -u 1 \"./src/**/*.html\" ./dist", "encrypt": "node --env-file=.env ./scripts/encrypt-data.js", "add-author-hash": "node --env-file=.env ./scripts/add-author-id-hash.js" + }, + "allowScripts": { + "esbuild@0.28.1": true, + "sqlite3@6.0.1": true } } diff --git a/src/events/guild/guildAuditLogEntryCreate.ts b/src/events/guild/guildAuditLogEntryCreate.ts index ca24628..59cf31f 100644 --- a/src/events/guild/guildAuditLogEntryCreate.ts +++ b/src/events/guild/guildAuditLogEntryCreate.ts @@ -1,5 +1,6 @@ -import { APIEmbedField, APIRole, AuditLogEvent, Guild, GuildAuditLogsEntry, RoleFlags, SnowflakeUtil } from 'discord.js'; +import { APIEmbedField, APIRole, AuditLogEvent, Client, Guild, GuildAuditLogsEntry, RoleFlags, SnowflakeUtil } from 'discord.js'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; import { CreateDefaultEmbed, formatChanges, formatExtras, formatSnowflake, getTargetType } from '../../utils'; const IGNORED_ACTIONS = [ @@ -33,10 +34,10 @@ async function shouldLogRoleChanges(entry: GuildAuditLogsEntry { + event = 'guildAuditLogEntryCreate' as const; - handler: async (entry: GuildAuditLogsEntry, guild: Guild) => { + async execute(context: Client, entry: GuildAuditLogsEntry, guild: Guild): Promise { if (!await shouldLog(entry, guild)) return; const settings = await Database.getOrCreateSettings(guild.id); @@ -95,4 +96,6 @@ export default { }] }); } -}; +} + +export default new GuildAuditLogEntryCreateEvent(); diff --git a/src/events/guild/guildBanRemove.ts b/src/events/guild/guildBanRemove.ts index 918d6a2..c1a93d5 100644 --- a/src/events/guild/guildBanRemove.ts +++ b/src/events/guild/guildBanRemove.ts @@ -1,10 +1,13 @@ -import { GuildBan } from 'discord.js'; +import { Client, GuildBan } from 'discord.js'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; -export default { - event: 'guildBanRemove', +class GuildBanRemoveEvent implements Event { + event = 'guildBanRemove' as const; - handler: async (ban: GuildBan) => { + async execute(context: Client, ban: GuildBan): Promise { await Database.removeBan(ban.user.id); } -}; +} + +export default new GuildBanRemoveEvent(); diff --git a/src/events/guild/guildCreate.ts b/src/events/guild/guildCreate.ts index 37c4144..81212fe 100644 --- a/src/events/guild/guildCreate.ts +++ b/src/events/guild/guildCreate.ts @@ -1,10 +1,13 @@ -import { Guild } from 'discord.js'; +import { Client, Guild } from 'discord.js'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; -export default { - event: 'guildCreate', +class GuildCreateEvent implements Event { + event = 'guildCreate' as const; - handler: async (guild: Guild) => { + async execute(context: Client, guild: Guild): Promise { await Database.getOrCreateSettings(guild.id); } -}; +} + +export default new GuildCreateEvent(); diff --git a/src/events/guild/guildMemberAdd.ts b/src/events/guild/guildMemberAdd.ts index d77b9f8..ea0ebce 100644 --- a/src/events/guild/guildMemberAdd.ts +++ b/src/events/guild/guildMemberAdd.ts @@ -1,27 +1,26 @@ -import { GuildMember, GuildTextBasedChannel } from 'discord.js'; +import { Client, GuildMember, GuildTextBasedChannel } from 'discord.js'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; import { getE621Alts } from '../../utils'; -export default { - event: 'guildMemberAdd', +class GuildMemberAddEvent implements Event { + event = 'guildMemberAdd' as const; - handler: async (member: GuildMember) => { + async execute(context: Client, member: GuildMember): Promise { const settings = await Database.getOrCreateSettings(member.guild.id); + if (!settings.new_member_channel_id) return; - if (settings.new_member_channel_id) { - const channel = await member.guild.channels.fetch(settings.new_member_channel_id) as GuildTextBasedChannel; + const channel = await member.guild.channels.fetch(settings.new_member_channel_id) as GuildTextBasedChannel; + if (!channel) return; - if (channel) { - const content = `${member.toString()}'s (${member.id}) e621 and discord account(s):\n${await getE621Alts(member.id, member.guild)}`; + const content = `${member.toString()}'s (${member.id}) e621 and discord account(s):\n${await getE621Alts(member.id, member.guild)}`; + channel.send(content).catch(console.error); - channel.send(content).catch(console.error); - - if (settings.moderator_channel_id && content.includes('[BANNED]')) { - const modChannel = await member.guild.channels.fetch(settings.moderator_channel_id) as GuildTextBasedChannel; - - if (modChannel) modChannel.send(`Member joined with banned alts:\n${content}`).catch(console.error); - } - } + if (settings.moderator_channel_id && content.includes('[BANNED]')) { + const modChannel = await member.guild.channels.fetch(settings.moderator_channel_id) as GuildTextBasedChannel; + if (modChannel) modChannel.send(`Member joined with banned alts:\n${content}`).catch(console.error); } } -}; +} + +export default new GuildMemberAddEvent(); diff --git a/src/events/message/messageCreate.ts b/src/events/message/messageCreate.ts index ef56945..301e48b 100644 --- a/src/events/message/messageCreate.ts +++ b/src/events/message/messageCreate.ts @@ -1,11 +1,13 @@ +import { Client } from 'discord.js'; import { config } from '../../config'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; import { ALLOWED_MIMETYPES, blacklistIfNecessary, calculateMD5FromURL, getE621PostByMd5, getPostUrl, md5Regex, Message, regexTesters, uniqueRegexMatches } from '../../utils'; -export default { - event: 'messageCreate', +class MessageCreateEvent implements Event { + event = 'messageCreate' as const; - handler: async (message: Message) => { + async execute(context: Client, message: Message): Promise { if (message.author.bot) return; if (message.inGuild()) await Database.putMessage(message); @@ -66,4 +68,6 @@ export default { await message.reply(responses.join('\n')); } } -}; \ No newline at end of file +} + +export default new MessageCreateEvent(); diff --git a/src/events/message/messageDelete.ts b/src/events/message/messageDelete.ts index 4802ecf..e467a49 100644 --- a/src/events/message/messageDelete.ts +++ b/src/events/message/messageDelete.ts @@ -1,11 +1,12 @@ -import { Message, PartialMessage } from 'discord.js'; +import { Client, Message, PartialMessage } from 'discord.js'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; import { logDeletion } from '../../utils'; -export default { - event: 'messageDelete', +class MessageDeleteEvent implements Event { + event = 'messageDelete' as const; - handler: async (message: Message | PartialMessage) => { + async execute(context: Client, message: Message | PartialMessage): Promise { const loggedMessage = await Database.getMessageWithRetry(message.id); if (!loggedMessage) return; @@ -13,4 +14,6 @@ export default { if (message.inGuild()) await logDeletion(loggedMessage, message); } -}; +} + +export default new MessageDeleteEvent(); diff --git a/src/events/message/messageDeleteBulk.ts b/src/events/message/messageDeleteBulk.ts index 6b45067..fbe2be2 100644 --- a/src/events/message/messageDeleteBulk.ts +++ b/src/events/message/messageDeleteBulk.ts @@ -1,18 +1,20 @@ -import { GuildTextBasedChannel, Message, OmitPartialGroupDMChannel, PartialMessage, ReadonlyCollection } from 'discord.js'; +import { Client, GuildTextBasedChannel, Message, OmitPartialGroupDMChannel, PartialMessage, ReadonlyCollection } from 'discord.js'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; import { logDeletion } from '../../utils'; -export default { - event: 'messageDeleteBulk', +class MessageDeleteBulkEvent implements Event { + event = 'messageDeleteBulk' as const; - handler: async (messages: ReadonlyCollection>, channel: GuildTextBasedChannel) => { + async execute(context: Client, messages: ReadonlyCollection>, channel: GuildTextBasedChannel): Promise { for (const message of messages.values()) { const loggedMessage = await Database.getMessageWithRetry(message.id); if (!loggedMessage) continue; await Database.removeMessge(message.id); - if (message.inGuild()) await logDeletion(loggedMessage, message); } } -}; +} + +export default new MessageDeleteBulkEvent(); diff --git a/src/events/message/messageUpdate.ts b/src/events/message/messageUpdate.ts index 376d6c1..e051505 100644 --- a/src/events/message/messageUpdate.ts +++ b/src/events/message/messageUpdate.ts @@ -1,12 +1,13 @@ -import { PartialMessage } from 'discord.js'; +import { Client, PartialMessage } from 'discord.js'; import { config } from '../../config'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; import { isEdited, logEdit, Message, regexTesters, uniqueRegexMatches } from '../../utils'; -export default { - event: 'messageUpdate', +class MessageUpdateEvent implements Event { + event = 'messageUpdate' as const; - handler: async (oldMessage: Message | PartialMessage, newMessage: Message) => { + async execute(context: Client, oldMessage: Message | PartialMessage, newMessage: Message): Promise { if (newMessage.author.bot) return; const loggedMessage = await Database.getMessageWithRetry(newMessage.id); @@ -31,39 +32,35 @@ export default { const hasMatches = test.regex.test(newMessage.content); test.regex.lastIndex = 0; - if (hasMatches) { - const oldMatches: RegExpExecArray[] = []; - const newMatches: RegExpExecArray[] = []; - let match: RegExpExecArray | null; + if (!hasMatches) continue; - while ((match = test.regex.exec(newMessage.content)) != null) { - newMatches.push(match); - } - test.regex.lastIndex = 0; + const oldMatches: RegExpExecArray[] = []; + const newMatches: RegExpExecArray[] = []; + let match: RegExpExecArray | null; - while ((match = test.regex.exec(loggedMessage.content)) != null) { - oldMatches.push(match); - } - test.regex.lastIndex = 0; + while ((match = test.regex.exec(newMessage.content)) != null) newMatches.push(match); + test.regex.lastIndex = 0; - const properMatches: RegExpExecArray[] = []; + while ((match = test.regex.exec(loggedMessage.content)) != null) oldMatches.push(match); + test.regex.lastIndex = 0; - for (const newMatch of newMatches) { - if (!oldMatches.find(m => m[1] == newMatch[1])) properMatches.push(newMatch); - } + const properMatches: RegExpExecArray[] = []; - if (properMatches.length == 0) continue; - - const response = await test.handler(newMessage, properMatches.filter(uniqueRegexMatches)); - - if (response === false) return; - - if (response !== true) responses.push(response as string); + for (const newMatch of newMatches) { + if (!oldMatches.find(m => m[1] == newMatch[1])) properMatches.push(newMatch); } + + if (properMatches.length == 0) continue; + const response = await test.handler(newMessage, properMatches.filter(uniqueRegexMatches)); + + if (response === false) return; + if (response !== true) responses.push(response as string); } if (responses.length > 0) { await newMessage.reply(responses.join('\n')); } } -}; \ No newline at end of file +} + +export default new MessageUpdateEvent(); diff --git a/src/events/thread/threadCreate.ts b/src/events/thread/threadCreate.ts index acdfa8d..b5e6e00 100644 --- a/src/events/thread/threadCreate.ts +++ b/src/events/thread/threadCreate.ts @@ -1,13 +1,16 @@ -import { AnyThreadChannel } from 'discord.js'; +import { AnyThreadChannel, Client } from 'discord.js'; +import { Event } from '../../types'; -export default { - event: 'threadCreate', +class ThreadCreateEvent implements Event { + event = 'threadCreate' as const; - handler: async (thread: AnyThreadChannel, newlyCreated: boolean) => { + async execute(context: Client, thread: AnyThreadChannel, _newlyCreated: boolean) { try { await thread.join(); } catch (e) { console.error(`Failed to join thread: ${thread.name} (${thread.id})`, e); } } -}; +} + +export default new ThreadCreateEvent(); diff --git a/src/events/voice/voiceStateUpdate.ts b/src/events/voice/voiceStateUpdate.ts index 3f1daf4..108dbea 100644 --- a/src/events/voice/voiceStateUpdate.ts +++ b/src/events/voice/voiceStateUpdate.ts @@ -1,5 +1,6 @@ -import { Guild, GuildTextBasedChannel, VoiceState } from 'discord.js'; +import { Client, Guild, GuildTextBasedChannel, VoiceState } from 'discord.js'; import { Database } from '../../shared/Database'; +import { Event } from '../../types'; import { CreateDefaultEmbed, SetSeverity } from '../../utils'; async function getVoiceLogsChannel(guild: Guild): Promise { @@ -22,7 +23,6 @@ async function memberJoinedChannel(_: VoiceState, state: VoiceState) { ...SetSeverity('success'), title: 'Joined Voice Channel', - fields: [ { name: 'Member', value: `<@${state.member?.id}>`, inline: false }, { name: 'Channel', value: `${state.channel?.name} (${state.channelId})`, inline: true }, @@ -61,7 +61,6 @@ async function memberMovedChannel(oldState: VoiceState, newState: VoiceState) { ...SetSeverity('warning'), title: 'Moved Voice Channel', - fields: [ { name: 'Member', value: `<@${newState.member?.id}>`, inline: false }, { name: 'Channel', value: `<#${oldState.channelId}> -> <#${newState.channelId}>`, inline: false }, @@ -70,10 +69,10 @@ async function memberMovedChannel(oldState: VoiceState, newState: VoiceState) { }); } -export default { - event: 'voiceStateUpdate', +class VoiceStateUpdateEvent implements Event { + event = 'voiceStateUpdate' as const; - handler: async (oldState: VoiceState, newState: VoiceState) => { + async execute(context: Client, oldState: VoiceState, newState: VoiceState): Promise { // Ignore non-movement voiceStateUpdate events. if (oldState.channelId === newState.channelId) return; @@ -84,4 +83,6 @@ export default { else if (newState.channelId == null && oldState.channelId != null) await memberLeftChannel(oldState, newState); } -}; +} + +export default new VoiceStateUpdateEvent(); diff --git a/src/index.ts b/src/index.ts index ed3f668..1b79185 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { Client as DiscordClient, GatewayIntentBits, MessageFlags, Partials } from 'discord.js'; +import { Client, Client as DiscordClient, GatewayIntentBits, MessageFlags, Partials } from 'discord.js'; import { config } from './config'; import events from './events'; import { Database } from './shared/Database'; @@ -151,7 +151,7 @@ client.on('clientReady', async () => { events.forEach(event => client.on(event.event, (...args) => - Promise.resolve((event.handler as (...args: unknown[]) => Promise)(...args)) + Promise.resolve((event.execute as (context: Client, ...args: unknown[]) => Promise)(client, ...args)) .catch(e => console.error(`Error in ${event.event} handler:`, e)) ) ); @@ -160,7 +160,6 @@ client.on('clientReady', async () => { console.log('Ready'); }); - client.on('error', console.error); process.on('uncaughtException', console.error); diff --git a/src/types/event.d.ts b/src/types/event.d.ts new file mode 100644 index 0000000..1abde8a --- /dev/null +++ b/src/types/event.d.ts @@ -0,0 +1,6 @@ +import { ClientEvents } from 'discord.js'; + +export interface Event { + readonly event: E; + execute(context: Context, ...args: ClientEvents[E]): Promise; +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index d867c83..61a0722 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,5 +1,6 @@ export * from './command.d'; export * from './database-types.d'; +export * from './event.d'; export * from './e621-types.d'; export * from './handler.d'; export * from './helper-types.d';