[Events] Events become classes with typings. (#19)

This commit is contained in:
2026-09-30 06:48:01 -07:00
committed by GitHub
parent 56f528c0c3
commit 1757ef2f8a
15 changed files with 119 additions and 91 deletions
+1
View File
@@ -4,6 +4,7 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "discordbot-ng",
"dependencies": { "dependencies": {
"@clynamic/dmark": "0.0.11", "@clynamic/dmark": "0.0.11",
"@redis/client": "5.1.0", "@redis/client": "5.1.0",
+4 -1
View File
@@ -1,6 +1,5 @@
{ {
"name": "discordbot-ng", "name": "discordbot-ng",
"devDependencies": { "devDependencies": {
"@eslint/js": "9.39.5", "@eslint/js": "9.39.5",
"@stylistic/eslint-plugin": "5.2.1", "@stylistic/eslint-plugin": "5.2.1",
@@ -34,5 +33,9 @@
"copyfiles": "copyfiles -u 1 \"./src/**/*.html\" ./dist", "copyfiles": "copyfiles -u 1 \"./src/**/*.html\" ./dist",
"encrypt": "node --env-file=.env ./scripts/encrypt-data.js", "encrypt": "node --env-file=.env ./scripts/encrypt-data.js",
"add-author-hash": "node --env-file=.env ./scripts/add-author-id-hash.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
} }
} }
+8 -5
View File
@@ -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 { Database } from '../../shared/Database';
import { Event } from '../../types';
import { CreateDefaultEmbed, formatChanges, formatExtras, formatSnowflake, getTargetType } from '../../utils'; import { CreateDefaultEmbed, formatChanges, formatExtras, formatSnowflake, getTargetType } from '../../utils';
const IGNORED_ACTIONS = [ const IGNORED_ACTIONS = [
@@ -33,10 +34,10 @@ async function shouldLogRoleChanges(entry: GuildAuditLogsEntry<AuditLogEvent.Mem
return false; return false;
} }
export default { class GuildAuditLogEntryCreateEvent implements Event<Client, 'guildAuditLogEntryCreate'> {
event: 'guildAuditLogEntryCreate', event = 'guildAuditLogEntryCreate' as const;
handler: async (entry: GuildAuditLogsEntry, guild: Guild) => { async execute(context: Client, entry: GuildAuditLogsEntry, guild: Guild): Promise<void> {
if (!await shouldLog(entry, guild)) return; if (!await shouldLog(entry, guild)) return;
const settings = await Database.getOrCreateSettings(guild.id); const settings = await Database.getOrCreateSettings(guild.id);
@@ -95,4 +96,6 @@ export default {
}] }]
}); });
} }
}; }
export default new GuildAuditLogEntryCreateEvent();
+8 -5
View File
@@ -1,10 +1,13 @@
import { GuildBan } from 'discord.js'; import { Client, GuildBan } from 'discord.js';
import { Database } from '../../shared/Database'; import { Database } from '../../shared/Database';
import { Event } from '../../types';
export default { class GuildBanRemoveEvent implements Event<Client, 'guildBanRemove'> {
event: 'guildBanRemove', event = 'guildBanRemove' as const;
handler: async (ban: GuildBan) => { async execute(context: Client<boolean>, ban: GuildBan): Promise<void> {
await Database.removeBan(ban.user.id); await Database.removeBan(ban.user.id);
} }
}; }
export default new GuildBanRemoveEvent();
+8 -5
View File
@@ -1,10 +1,13 @@
import { Guild } from 'discord.js'; import { Client, Guild } from 'discord.js';
import { Database } from '../../shared/Database'; import { Database } from '../../shared/Database';
import { Event } from '../../types';
export default { class GuildCreateEvent implements Event<Client, 'guildCreate'> {
event: 'guildCreate', event = 'guildCreate' as const;
handler: async (guild: Guild) => { async execute(context: Client<boolean>, guild: Guild): Promise<void> {
await Database.getOrCreateSettings(guild.id); await Database.getOrCreateSettings(guild.id);
} }
}; }
export default new GuildCreateEvent();
+9 -10
View File
@@ -1,27 +1,26 @@
import { GuildMember, GuildTextBasedChannel } from 'discord.js'; import { Client, GuildMember, GuildTextBasedChannel } from 'discord.js';
import { Database } from '../../shared/Database'; import { Database } from '../../shared/Database';
import { Event } from '../../types';
import { getE621Alts } from '../../utils'; import { getE621Alts } from '../../utils';
export default { class GuildMemberAddEvent implements Event<Client, 'guildMemberAdd'> {
event: 'guildMemberAdd', event = 'guildMemberAdd' as const;
handler: async (member: GuildMember) => { async execute(context: Client<boolean>, member: GuildMember): Promise<void> {
const settings = await Database.getOrCreateSettings(member.guild.id); 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]')) { if (settings.moderator_channel_id && content.includes('[BANNED]')) {
const modChannel = await member.guild.channels.fetch(settings.moderator_channel_id) as GuildTextBasedChannel; 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 (modChannel) modChannel.send(`Member joined with banned alts:\n${content}`).catch(console.error);
} }
} }
} }
}
}; export default new GuildMemberAddEvent();
+8 -4
View File
@@ -1,11 +1,13 @@
import { Client } from 'discord.js';
import { config } from '../../config'; import { config } from '../../config';
import { Database } from '../../shared/Database'; import { Database } from '../../shared/Database';
import { Event } from '../../types';
import { ALLOWED_MIMETYPES, blacklistIfNecessary, calculateMD5FromURL, getE621PostByMd5, getPostUrl, md5Regex, Message, regexTesters, uniqueRegexMatches } from '../../utils'; import { ALLOWED_MIMETYPES, blacklistIfNecessary, calculateMD5FromURL, getE621PostByMd5, getPostUrl, md5Regex, Message, regexTesters, uniqueRegexMatches } from '../../utils';
export default { class MessageCreateEvent implements Event<Client, 'messageCreate'> {
event: 'messageCreate', event = 'messageCreate' as const;
handler: async (message: Message) => { async execute(context: Client<boolean>, message: Message): Promise<void> {
if (message.author.bot) return; if (message.author.bot) return;
if (message.inGuild()) await Database.putMessage(message); if (message.inGuild()) await Database.putMessage(message);
@@ -66,4 +68,6 @@ export default {
await message.reply(responses.join('\n')); await message.reply(responses.join('\n'));
} }
} }
}; }
export default new MessageCreateEvent();
+8 -5
View File
@@ -1,11 +1,12 @@
import { Message, PartialMessage } from 'discord.js'; import { Client, Message, PartialMessage } from 'discord.js';
import { Database } from '../../shared/Database'; import { Database } from '../../shared/Database';
import { Event } from '../../types';
import { logDeletion } from '../../utils'; import { logDeletion } from '../../utils';
export default { class MessageDeleteEvent implements Event<Client, 'messageDelete'> {
event: 'messageDelete', event = 'messageDelete' as const;
handler: async (message: Message | PartialMessage) => { async execute(context: Client<boolean>, message: Message | PartialMessage): Promise<void> {
const loggedMessage = await Database.getMessageWithRetry(message.id); const loggedMessage = await Database.getMessageWithRetry(message.id);
if (!loggedMessage) return; if (!loggedMessage) return;
@@ -13,4 +14,6 @@ export default {
if (message.inGuild()) await logDeletion(loggedMessage, message); if (message.inGuild()) await logDeletion(loggedMessage, message);
} }
}; }
export default new MessageDeleteEvent();
+8 -6
View File
@@ -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 { Database } from '../../shared/Database';
import { Event } from '../../types';
import { logDeletion } from '../../utils'; import { logDeletion } from '../../utils';
export default { class MessageDeleteBulkEvent implements Event<Client, 'messageDeleteBulk'> {
event: 'messageDeleteBulk', event = 'messageDeleteBulk' as const;
handler: async (messages: ReadonlyCollection<string, Message | OmitPartialGroupDMChannel<PartialMessage>>, channel: GuildTextBasedChannel) => { async execute(context: Client<boolean>, messages: ReadonlyCollection<string, Message | OmitPartialGroupDMChannel<PartialMessage>>, channel: GuildTextBasedChannel): Promise<void> {
for (const message of messages.values()) { for (const message of messages.values()) {
const loggedMessage = await Database.getMessageWithRetry(message.id); const loggedMessage = await Database.getMessageWithRetry(message.id);
if (!loggedMessage) continue; if (!loggedMessage) continue;
await Database.removeMessge(message.id); await Database.removeMessge(message.id);
if (message.inGuild()) await logDeletion(loggedMessage, message); if (message.inGuild()) await logDeletion(loggedMessage, message);
} }
} }
}; }
export default new MessageDeleteBulkEvent();
+12 -15
View File
@@ -1,12 +1,13 @@
import { PartialMessage } from 'discord.js'; import { Client, PartialMessage } from 'discord.js';
import { config } from '../../config'; import { config } from '../../config';
import { Database } from '../../shared/Database'; import { Database } from '../../shared/Database';
import { Event } from '../../types';
import { isEdited, logEdit, Message, regexTesters, uniqueRegexMatches } from '../../utils'; import { isEdited, logEdit, Message, regexTesters, uniqueRegexMatches } from '../../utils';
export default { class MessageUpdateEvent implements Event<Client, 'messageUpdate'> {
event: 'messageUpdate', event = 'messageUpdate' as const;
handler: async (oldMessage: Message | PartialMessage, newMessage: Message) => { async execute(context: Client<boolean>, oldMessage: Message | PartialMessage, newMessage: Message): Promise<void> {
if (newMessage.author.bot) return; if (newMessage.author.bot) return;
const loggedMessage = await Database.getMessageWithRetry(newMessage.id); const loggedMessage = await Database.getMessageWithRetry(newMessage.id);
@@ -31,19 +32,16 @@ export default {
const hasMatches = test.regex.test(newMessage.content); const hasMatches = test.regex.test(newMessage.content);
test.regex.lastIndex = 0; test.regex.lastIndex = 0;
if (hasMatches) { if (!hasMatches) continue;
const oldMatches: RegExpExecArray[] = []; const oldMatches: RegExpExecArray[] = [];
const newMatches: RegExpExecArray[] = []; const newMatches: RegExpExecArray[] = [];
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
while ((match = test.regex.exec(newMessage.content)) != null) { while ((match = test.regex.exec(newMessage.content)) != null) newMatches.push(match);
newMatches.push(match);
}
test.regex.lastIndex = 0; test.regex.lastIndex = 0;
while ((match = test.regex.exec(loggedMessage.content)) != null) { while ((match = test.regex.exec(loggedMessage.content)) != null) oldMatches.push(match);
oldMatches.push(match);
}
test.regex.lastIndex = 0; test.regex.lastIndex = 0;
const properMatches: RegExpExecArray[] = []; const properMatches: RegExpExecArray[] = [];
@@ -53,17 +51,16 @@ export default {
} }
if (properMatches.length == 0) continue; if (properMatches.length == 0) continue;
const response = await test.handler(newMessage, properMatches.filter(uniqueRegexMatches)); const response = await test.handler(newMessage, properMatches.filter(uniqueRegexMatches));
if (response === false) return; if (response === false) return;
if (response !== true) responses.push(response as string); if (response !== true) responses.push(response as string);
} }
}
if (responses.length > 0) { if (responses.length > 0) {
await newMessage.reply(responses.join('\n')); await newMessage.reply(responses.join('\n'));
} }
} }
}; }
export default new MessageUpdateEvent();
+8 -5
View File
@@ -1,13 +1,16 @@
import { AnyThreadChannel } from 'discord.js'; import { AnyThreadChannel, Client } from 'discord.js';
import { Event } from '../../types';
export default { class ThreadCreateEvent implements Event<Client, 'threadCreate'> {
event: 'threadCreate', event = 'threadCreate' as const;
handler: async (thread: AnyThreadChannel, newlyCreated: boolean) => { async execute(context: Client<boolean>, thread: AnyThreadChannel, _newlyCreated: boolean) {
try { try {
await thread.join(); await thread.join();
} catch (e) { } catch (e) {
console.error(`Failed to join thread: ${thread.name} (${thread.id})`, e); console.error(`Failed to join thread: ${thread.name} (${thread.id})`, e);
} }
} }
}; }
export default new ThreadCreateEvent();
+8 -7
View File
@@ -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 { Database } from '../../shared/Database';
import { Event } from '../../types';
import { CreateDefaultEmbed, SetSeverity } from '../../utils'; import { CreateDefaultEmbed, SetSeverity } from '../../utils';
async function getVoiceLogsChannel(guild: Guild): Promise<GuildTextBasedChannel | undefined> { async function getVoiceLogsChannel(guild: Guild): Promise<GuildTextBasedChannel | undefined> {
@@ -22,7 +23,6 @@ async function memberJoinedChannel(_: VoiceState, state: VoiceState) {
...SetSeverity('success'), ...SetSeverity('success'),
title: 'Joined Voice Channel', title: 'Joined Voice Channel',
fields: [ fields: [
{ name: 'Member', value: `<@${state.member?.id}>`, inline: false }, { name: 'Member', value: `<@${state.member?.id}>`, inline: false },
{ name: 'Channel', value: `${state.channel?.name} (${state.channelId})`, inline: true }, { name: 'Channel', value: `${state.channel?.name} (${state.channelId})`, inline: true },
@@ -61,7 +61,6 @@ async function memberMovedChannel(oldState: VoiceState, newState: VoiceState) {
...SetSeverity('warning'), ...SetSeverity('warning'),
title: 'Moved Voice Channel', title: 'Moved Voice Channel',
fields: [ fields: [
{ name: 'Member', value: `<@${newState.member?.id}>`, inline: false }, { name: 'Member', value: `<@${newState.member?.id}>`, inline: false },
{ name: 'Channel', value: `<#${oldState.channelId}> -> <#${newState.channelId}>`, inline: false }, { name: 'Channel', value: `<#${oldState.channelId}> -> <#${newState.channelId}>`, inline: false },
@@ -70,10 +69,10 @@ async function memberMovedChannel(oldState: VoiceState, newState: VoiceState) {
}); });
} }
export default { class VoiceStateUpdateEvent implements Event<Client, 'voiceStateUpdate'> {
event: 'voiceStateUpdate', event = 'voiceStateUpdate' as const;
handler: async (oldState: VoiceState, newState: VoiceState) => { async execute(context: Client<boolean>, oldState: VoiceState, newState: VoiceState): Promise<void> {
// Ignore non-movement voiceStateUpdate events. // Ignore non-movement voiceStateUpdate events.
if (oldState.channelId === newState.channelId) return; if (oldState.channelId === newState.channelId) return;
@@ -84,4 +83,6 @@ export default {
else if (newState.channelId == null && oldState.channelId != null) else if (newState.channelId == null && oldState.channelId != null)
await memberLeftChannel(oldState, newState); await memberLeftChannel(oldState, newState);
} }
}; }
export default new VoiceStateUpdateEvent();
+2 -3
View File
@@ -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 { config } from './config';
import events from './events'; import events from './events';
import { Database } from './shared/Database'; import { Database } from './shared/Database';
@@ -151,7 +151,7 @@ client.on('clientReady', async () => {
events.forEach(event => events.forEach(event =>
client.on(event.event, (...args) => client.on(event.event, (...args) =>
Promise.resolve((event.handler as (...args: unknown[]) => Promise<void>)(...args)) Promise.resolve((event.execute as (context: Client, ...args: unknown[]) => Promise<void>)(client, ...args))
.catch(e => console.error(`Error in ${event.event} handler:`, e)) .catch(e => console.error(`Error in ${event.event} handler:`, e))
) )
); );
@@ -160,7 +160,6 @@ client.on('clientReady', async () => {
console.log('Ready'); console.log('Ready');
}); });
client.on('error', console.error); client.on('error', console.error);
process.on('uncaughtException', console.error); process.on('uncaughtException', console.error);
+6
View File
@@ -0,0 +1,6 @@
import { ClientEvents } from 'discord.js';
export interface Event<Context, E extends keyof ClientEvents> {
readonly event: E;
execute(context: Context, ...args: ClientEvents[E]): Promise<void>;
}
+1
View File
@@ -1,5 +1,6 @@
export * from './command.d'; export * from './command.d';
export * from './database-types.d'; export * from './database-types.d';
export * from './event.d';
export * from './e621-types.d'; export * from './e621-types.d';
export * from './handler.d'; export * from './handler.d';
export * from './helper-types.d'; export * from './helper-types.d';