[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,
"packages": {
"": {
"name": "discordbot-ng",
"dependencies": {
"@clynamic/dmark": "0.0.11",
"@redis/client": "5.1.0",
+4 -1
View File
@@ -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
}
}
+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 { Event } from '../../types';
import { CreateDefaultEmbed, formatChanges, formatExtras, formatSnowflake, getTargetType } from '../../utils';
const IGNORED_ACTIONS = [
@@ -33,10 +34,10 @@ async function shouldLogRoleChanges(entry: GuildAuditLogsEntry<AuditLogEvent.Mem
return false;
}
export default {
event: 'guildAuditLogEntryCreate',
class GuildAuditLogEntryCreateEvent implements Event<Client, '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;
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 { Event } from '../../types';
export default {
event: 'guildBanRemove',
class GuildBanRemoveEvent implements Event<Client, 'guildBanRemove'> {
event = 'guildBanRemove' as const;
handler: async (ban: GuildBan) => {
async execute(context: Client<boolean>, ban: GuildBan): Promise<void> {
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 { Event } from '../../types';
export default {
event: 'guildCreate',
class GuildCreateEvent implements Event<Client, 'guildCreate'> {
event = 'guildCreate' as const;
handler: async (guild: Guild) => {
async execute(context: Client<boolean>, guild: Guild): Promise<void> {
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 { Event } from '../../types';
import { getE621Alts } from '../../utils';
export default {
event: 'guildMemberAdd',
class GuildMemberAddEvent implements Event<Client, '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);
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;
if (!channel) return;
if (channel) {
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);
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();
+8 -4
View File
@@ -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<Client, '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.inGuild()) await Database.putMessage(message);
@@ -66,4 +68,6 @@ export default {
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 { Event } from '../../types';
import { logDeletion } from '../../utils';
export default {
event: 'messageDelete',
class MessageDeleteEvent implements Event<Client, '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);
if (!loggedMessage) return;
@@ -13,4 +14,6 @@ export default {
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 { Event } from '../../types';
import { logDeletion } from '../../utils';
export default {
event: 'messageDeleteBulk',
class MessageDeleteBulkEvent implements Event<Client, '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()) {
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();
+12 -15
View File
@@ -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<Client, '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;
const loggedMessage = await Database.getMessageWithRetry(newMessage.id);
@@ -31,19 +32,16 @@ export default {
const hasMatches = test.regex.test(newMessage.content);
test.regex.lastIndex = 0;
if (hasMatches) {
if (!hasMatches) continue;
const oldMatches: RegExpExecArray[] = [];
const newMatches: RegExpExecArray[] = [];
let match: RegExpExecArray | null;
while ((match = test.regex.exec(newMessage.content)) != null) {
newMatches.push(match);
}
while ((match = test.regex.exec(newMessage.content)) != null) newMatches.push(match);
test.regex.lastIndex = 0;
while ((match = test.regex.exec(loggedMessage.content)) != null) {
oldMatches.push(match);
}
while ((match = test.regex.exec(loggedMessage.content)) != null) oldMatches.push(match);
test.regex.lastIndex = 0;
const properMatches: RegExpExecArray[] = [];
@@ -53,17 +51,16 @@ export default {
}
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'));
}
}
};
}
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 {
event: 'threadCreate',
class ThreadCreateEvent implements Event<Client, 'threadCreate'> {
event = 'threadCreate' as const;
handler: async (thread: AnyThreadChannel, newlyCreated: boolean) => {
async execute(context: Client<boolean>, 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();
+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 { Event } from '../../types';
import { CreateDefaultEmbed, SetSeverity } from '../../utils';
async function getVoiceLogsChannel(guild: Guild): Promise<GuildTextBasedChannel | undefined> {
@@ -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<Client, '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.
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();
+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 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<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))
)
);
@@ -160,7 +160,6 @@ client.on('clientReady', async () => {
console.log('Ready');
});
client.on('error', 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 './database-types.d';
export * from './event.d';
export * from './e621-types.d';
export * from './handler.d';
export * from './helper-types.d';