From b9faf8d0a6d7b9b4b771e35976b0a8b9fee08c96 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Sun, 24 May 2026 03:49:28 +0800 Subject: [PATCH 1/4] new scheduler? in 2026? nahhh --- src/index.ts | 14 ++++------- src/scheduler/index.ts | 24 +++++++++++++++++++ .../tasks/check-expired-bans-task.ts | 12 ++++++++++ .../tasks/close-stale-tickets-task.ts | 12 ++++++++++ src/scheduler/tasks/index.ts | 7 ++++++ src/types/index.d.ts | 1 + src/types/scheduler.d.ts | 8 +++++++ 7 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 src/scheduler/index.ts create mode 100644 src/scheduler/tasks/check-expired-bans-task.ts create mode 100644 src/scheduler/tasks/close-stale-tickets-task.ts create mode 100644 src/scheduler/tasks/index.ts create mode 100644 src/types/scheduler.d.ts diff --git a/src/index.ts b/src/index.ts index ccc5196..e0cf996 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,9 @@ import { handleAuditLogCreate, handleBanRemove, handleBulkMessageDelete, handleG import { Database } from './shared/Database'; import { openRedisClient } from './shared/RedisClient'; import { Handler } from './types'; -import { checkExpiredBans, closeOldTickets, initIfNecessary, loadHandlersFrom, refreshCommands } from './utils'; +import { initIfNecessary, loadHandlersFrom, refreshCommands } from './utils'; import { initializeWebserver } from './webserver'; +import { Scheduler, ScheduledTasks } from './scheduler'; let ready = false; @@ -28,6 +29,8 @@ const client = new DiscordClient({ } }); +const scheduler: Scheduler = new Scheduler(client); + const commands: Handler[] = []; const contextMenus: Handler[] = []; const buttons: Handler[] = []; @@ -135,16 +138,9 @@ client.on('clientReady', async () => { await initializeWebserver(client); - // Check for expired bans every 5 minutes - checkExpiredBans(client); - setInterval(checkExpiredBans.bind(null, client), 300000); - - // Close old tickets every hour - closeOldTickets(client); - setInterval(closeOldTickets.bind(null, client), 3.6e+6); + ScheduledTasks.forEach((task) => scheduler.add(task)); ready = true; - console.log('Ready'); }); diff --git a/src/scheduler/index.ts b/src/scheduler/index.ts new file mode 100644 index 0000000..c852193 --- /dev/null +++ b/src/scheduler/index.ts @@ -0,0 +1,24 @@ +import { Client } from "discord.js"; +import ScheduledTasks from "./tasks"; +import { Task } from "../types"; + +class Scheduler { + private tasks = new Map(); + private context: Client; + + constructor(context: Client) { + this.context = context; + } + + add(task: Task) { + const interval = setInterval(() => task.handle(this.context), task.interval); + if (task.firstRun) interval._onTimeout(); + + this.tasks.set(task, interval); + } +} + +export { + Scheduler, + ScheduledTasks +} \ No newline at end of file diff --git a/src/scheduler/tasks/check-expired-bans-task.ts b/src/scheduler/tasks/check-expired-bans-task.ts new file mode 100644 index 0000000..8882b08 --- /dev/null +++ b/src/scheduler/tasks/check-expired-bans-task.ts @@ -0,0 +1,12 @@ +import { Client } from "discord.js"; +import { Task } from "../../types"; +import { checkExpiredBans } from "../../utils"; + +const checkExpiredBansTask: Task = { + interval: 300000, + firstRun: true, + + handle: async (context: Client) => await checkExpiredBans(context) +}; + +export default checkExpiredBansTask; \ No newline at end of file diff --git a/src/scheduler/tasks/close-stale-tickets-task.ts b/src/scheduler/tasks/close-stale-tickets-task.ts new file mode 100644 index 0000000..25e4997 --- /dev/null +++ b/src/scheduler/tasks/close-stale-tickets-task.ts @@ -0,0 +1,12 @@ +import { Client } from "discord.js"; +import { Task } from "../../types"; +import { closeOldTickets } from "../../utils"; + +const closeStaleTicketsTask: Task = { + interval: 3.6e+6, + firstRun: true, + + handle: async (context: Client) => await closeOldTickets(context) +}; + +export default closeStaleTicketsTask; \ No newline at end of file diff --git a/src/scheduler/tasks/index.ts b/src/scheduler/tasks/index.ts new file mode 100644 index 0000000..77839ca --- /dev/null +++ b/src/scheduler/tasks/index.ts @@ -0,0 +1,7 @@ +import checkExpiredBansTask from './check-expired-bans-task'; +import closeStaleTicketsTask from './close-stale-tickets-task'; + +export default [ + closeStaleTicketsTask, + checkExpiredBansTask, +]; \ No newline at end of file diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 45567b0..bd9ddc2 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -3,3 +3,4 @@ export * from './database-types.d'; export * from './e621-types.d'; export * from './handler.d'; export * from './helper-types.d'; +export * from './scheduler.d'; \ No newline at end of file diff --git a/src/types/scheduler.d.ts b/src/types/scheduler.d.ts new file mode 100644 index 0000000..2e71161 --- /dev/null +++ b/src/types/scheduler.d.ts @@ -0,0 +1,8 @@ +import { Client } from "discord.js"; + +export type Task = { + interval: number; + firstRun: boolean; + + handle: (context: Client) => Promise; +}; \ No newline at end of file From 0d2823401c9edf8b68a898c7b24cbe0c795c241a Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Sun, 24 May 2026 05:16:51 +0800 Subject: [PATCH 2/4] organise imports --- src/index.ts | 2 +- src/scheduler/index.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index e0cf996..4b9cb20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ import { Client as DiscordClient, GatewayIntentBits, MessageFlags, Partials } from 'discord.js'; import { config } from './config'; import { handleAuditLogCreate, handleBanRemove, handleBulkMessageDelete, handleGuildCreate, handleMemberJoin, handleMessageCreate, handleMessageDelete, handleMessageUpdate, handleThreadCreate, handleVoiceStateUpdate } from './events'; +import { ScheduledTasks, Scheduler } from './scheduler'; import { Database } from './shared/Database'; import { openRedisClient } from './shared/RedisClient'; import { Handler } from './types'; import { initIfNecessary, loadHandlersFrom, refreshCommands } from './utils'; import { initializeWebserver } from './webserver'; -import { Scheduler, ScheduledTasks } from './scheduler'; let ready = false; diff --git a/src/scheduler/index.ts b/src/scheduler/index.ts index c852193..e4fa644 100644 --- a/src/scheduler/index.ts +++ b/src/scheduler/index.ts @@ -1,6 +1,6 @@ import { Client } from "discord.js"; -import ScheduledTasks from "./tasks"; import { Task } from "../types"; +import ScheduledTasks from "./tasks"; class Scheduler { private tasks = new Map(); @@ -19,6 +19,5 @@ class Scheduler { } export { - Scheduler, - ScheduledTasks -} \ No newline at end of file + ScheduledTasks, Scheduler +}; From 79487818c23c07e96c12b533e4dbc3883d3666d5 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Sun, 24 May 2026 05:40:05 +0800 Subject: [PATCH 3/4] Update Task from type to interface. --- src/scheduler/tasks/check-expired-bans-task.ts | 14 ++++++++------ src/scheduler/tasks/close-stale-tickets-task.ts | 14 ++++++++------ src/types/scheduler.d.ts | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/scheduler/tasks/check-expired-bans-task.ts b/src/scheduler/tasks/check-expired-bans-task.ts index 8882b08..9e353b9 100644 --- a/src/scheduler/tasks/check-expired-bans-task.ts +++ b/src/scheduler/tasks/check-expired-bans-task.ts @@ -2,11 +2,13 @@ import { Client } from "discord.js"; import { Task } from "../../types"; import { checkExpiredBans } from "../../utils"; -const checkExpiredBansTask: Task = { - interval: 300000, - firstRun: true, +class CheckExpiredBansTask implements Task { + interval: number = 300000; + firstRun: boolean = true; - handle: async (context: Client) => await checkExpiredBans(context) -}; + async handle(context: Client): Promise { + await checkExpiredBans(context); + } +} -export default checkExpiredBansTask; \ No newline at end of file +export default new CheckExpiredBansTask(); \ No newline at end of file diff --git a/src/scheduler/tasks/close-stale-tickets-task.ts b/src/scheduler/tasks/close-stale-tickets-task.ts index 25e4997..8ff49e7 100644 --- a/src/scheduler/tasks/close-stale-tickets-task.ts +++ b/src/scheduler/tasks/close-stale-tickets-task.ts @@ -2,11 +2,13 @@ import { Client } from "discord.js"; import { Task } from "../../types"; import { closeOldTickets } from "../../utils"; -const closeStaleTicketsTask: Task = { - interval: 3.6e+6, - firstRun: true, +class CloseStaleTicketsTask implements Task { + interval: number = 3.6e6; + firstRun: boolean = true; - handle: async (context: Client) => await closeOldTickets(context) -}; + async handle(context: Client): Promise { + await closeOldTickets(context); + } +} -export default closeStaleTicketsTask; \ No newline at end of file +export default new CloseStaleTicketsTask(); \ No newline at end of file diff --git a/src/types/scheduler.d.ts b/src/types/scheduler.d.ts index 2e71161..10e28d1 100644 --- a/src/types/scheduler.d.ts +++ b/src/types/scheduler.d.ts @@ -1,8 +1,8 @@ import { Client } from "discord.js"; -export type Task = { +export interface Task { interval: number; firstRun: boolean; - handle: (context: Client) => Promise; + handle(context: Client): Promise; }; \ No newline at end of file From 0985712a1de9fb07b6816e5da2484a80bc1665ad Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Sat, 30 May 2026 16:38:48 +0800 Subject: [PATCH 4/4] Update changed files to use LF instead of CRLF. See the following commits for more info: - https://github.com/e621ng/discordbot-ng/commit/fdcb193e317b764755dea4a054a1b0a5adf745f2 - https://github.com/e621ng/discordbot-ng/commit/66d4bd49266a634436ff075c6765083853976d47 - https://github.com/e621ng/discordbot-ng/commit/6ad3ee5f23df319c09fdb52e6a8cf6e073a0bb3b --- src/index.ts | 322 +++++++++--------- .../tasks/check-expired-bans-task.ts | 2 +- .../tasks/close-stale-tickets-task.ts | 2 +- src/scheduler/tasks/index.ts | 2 +- src/types/index.d.ts | 12 +- src/types/scheduler.d.ts | 2 +- 6 files changed, 171 insertions(+), 171 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4b9cb20..25c0379 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,161 +1,161 @@ -import { Client as DiscordClient, GatewayIntentBits, MessageFlags, Partials } from 'discord.js'; -import { config } from './config'; -import { handleAuditLogCreate, handleBanRemove, handleBulkMessageDelete, handleGuildCreate, handleMemberJoin, handleMessageCreate, handleMessageDelete, handleMessageUpdate, handleThreadCreate, handleVoiceStateUpdate } from './events'; -import { ScheduledTasks, Scheduler } from './scheduler'; -import { Database } from './shared/Database'; -import { openRedisClient } from './shared/RedisClient'; -import { Handler } from './types'; -import { initIfNecessary, loadHandlersFrom, refreshCommands } from './utils'; -import { initializeWebserver } from './webserver'; - -let ready = false; - -console.log('Starting...'); - -const client = new DiscordClient({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildModeration, - GatewayIntentBits.GuildVoiceStates, - GatewayIntentBits.MessageContent - ], - partials: [Partials.Message, Partials.GuildMember, Partials.User, Partials.Channel], - rest: { timeout: 30000 }, - allowedMentions: { - parse: [], - repliedUser: false - } -}); - -const scheduler: Scheduler = new Scheduler(client); - -const commands: Handler[] = []; -const contextMenus: Handler[] = []; -const buttons: Handler[] = []; -const modals: Handler[] = []; -const menus: Handler[] = []; - -loadHandlersFrom('commands', commands); -loadHandlersFrom('context-menus', contextMenus); -loadHandlersFrom('buttons', buttons); -loadHandlersFrom('modals', modals); -loadHandlersFrom('menus', menus); - -// Due to their reliance on each other, these two events (interactionCreate, and ready) have to stay here. -// Alternatively, they can move to another single file. Or use static classes. -client.on('interactionCreate', async (interaction) => { - if (!ready) { - if ( - interaction.isChatInputCommand() - || interaction.isContextMenuCommand() - || interaction.isButton() - || interaction.isModalSubmit() - || interaction.isAnySelectMenu() - ) - interaction.reply({ - content: 'Bot is still starting up. Please wait a few seconds.', - flags: [MessageFlags.Ephemeral], - }); - - return; - } - - if (interaction.isChatInputCommand()) { - // Handle chat - for (const command of commands) { - if (interaction.commandName == command.name) { - command.handler(client, interaction); - return; - } - } - } else if (interaction.isContextMenuCommand()) { - // Handle context menu commands. - for (const command of contextMenus) { - if (interaction.commandName == command.name) { - command.handler(client, interaction); - return; - } - } - } else if (interaction.isAutocomplete()) { - // Handle autocomplete requests. - for (const command of commands) { - if (interaction.commandName == command.name) { - if (command.autoComplete) { - command - .autoComplete(client, interaction) - .catch(e => console.error(e)); - } - return; - } - } - } else if (interaction.isButton()) { - // Handle button presses. - const id = interaction.customId.split('_')[0]; - - for (const button of buttons) { - if (id == button.name) { - button.handler(client, interaction, ...interaction.customId.split('_').slice(1)); - return; - } - } - } else if (interaction.isModalSubmit()) { - // Handle modal submissions. - const id = interaction.customId.split('_')[0]; - - for (const modal of modals) { - if (id == modal.name) { - modal.handler(client, interaction, ...interaction.customId.split('_').slice(1)); - return; - } - } - } else if (interaction.isAnySelectMenu()) { - // Handle menu selections. - const id = interaction.customId.split('_')[0]; - - for (const menu of menus) { - if (id == menu.name) { - menu.handler(client, interaction, ...interaction.customId.split('_').slice(1)); - return; - } - } - } -}); - -client.on('clientReady', async () => { - console.log(`Logged in as ${client.user!.tag}!`); - - await refreshCommands(client); - - await initIfNecessary(client, commands); - await initIfNecessary(client, buttons); - await initIfNecessary(client, modals); - await initIfNecessary(client, menus); - - await Database.open('./data/discord-main.db'); - await openRedisClient(config.REDIS_URL!, client); - - await initializeWebserver(client); - - ScheduledTasks.forEach((task) => scheduler.add(task)); - - ready = true; - console.log('Ready'); -}); - -client.on('guildAuditLogEntryCreate', handleAuditLogCreate); -client.on('guildBanRemove', handleBanRemove); -client.on('guildCreate', handleGuildCreate); -client.on('guildMemberAdd', handleMemberJoin); -client.on('messageCreate', handleMessageCreate); -client.on('messageDelete', handleMessageDelete); -client.on('messageDeleteBulk', handleBulkMessageDelete); -client.on('messageUpdate', handleMessageUpdate); -client.on('threadCreate', handleThreadCreate); -client.on('voiceStateUpdate', handleVoiceStateUpdate); - -client.on('error', console.error); -process.on('uncaughtException', console.error); - -client.login(config.DISCORD_TOKEN); \ No newline at end of file +import { Client as DiscordClient, GatewayIntentBits, MessageFlags, Partials } from 'discord.js'; +import { config } from './config'; +import { handleAuditLogCreate, handleBanRemove, handleBulkMessageDelete, handleGuildCreate, handleMemberJoin, handleMessageCreate, handleMessageDelete, handleMessageUpdate, handleThreadCreate, handleVoiceStateUpdate } from './events'; +import { ScheduledTasks, Scheduler } from './scheduler'; +import { Database } from './shared/Database'; +import { openRedisClient } from './shared/RedisClient'; +import { Handler } from './types'; +import { initIfNecessary, loadHandlersFrom, refreshCommands } from './utils'; +import { initializeWebserver } from './webserver'; + +let ready = false; + +console.log('Starting...'); + +const client = new DiscordClient({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildModeration, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.MessageContent + ], + partials: [Partials.Message, Partials.GuildMember, Partials.User, Partials.Channel], + rest: { timeout: 30000 }, + allowedMentions: { + parse: [], + repliedUser: false + } +}); + +const scheduler: Scheduler = new Scheduler(client); + +const commands: Handler[] = []; +const contextMenus: Handler[] = []; +const buttons: Handler[] = []; +const modals: Handler[] = []; +const menus: Handler[] = []; + +loadHandlersFrom('commands', commands); +loadHandlersFrom('context-menus', contextMenus); +loadHandlersFrom('buttons', buttons); +loadHandlersFrom('modals', modals); +loadHandlersFrom('menus', menus); + +// Due to their reliance on each other, these two events (interactionCreate, and ready) have to stay here. +// Alternatively, they can move to another single file. Or use static classes. +client.on('interactionCreate', async (interaction) => { + if (!ready) { + if ( + interaction.isChatInputCommand() + || interaction.isContextMenuCommand() + || interaction.isButton() + || interaction.isModalSubmit() + || interaction.isAnySelectMenu() + ) + interaction.reply({ + content: 'Bot is still starting up. Please wait a few seconds.', + flags: [MessageFlags.Ephemeral], + }); + + return; + } + + if (interaction.isChatInputCommand()) { + // Handle chat + for (const command of commands) { + if (interaction.commandName == command.name) { + command.handler(client, interaction); + return; + } + } + } else if (interaction.isContextMenuCommand()) { + // Handle context menu commands. + for (const command of contextMenus) { + if (interaction.commandName == command.name) { + command.handler(client, interaction); + return; + } + } + } else if (interaction.isAutocomplete()) { + // Handle autocomplete requests. + for (const command of commands) { + if (interaction.commandName == command.name) { + if (command.autoComplete) { + command + .autoComplete(client, interaction) + .catch(e => console.error(e)); + } + return; + } + } + } else if (interaction.isButton()) { + // Handle button presses. + const id = interaction.customId.split('_')[0]; + + for (const button of buttons) { + if (id == button.name) { + button.handler(client, interaction, ...interaction.customId.split('_').slice(1)); + return; + } + } + } else if (interaction.isModalSubmit()) { + // Handle modal submissions. + const id = interaction.customId.split('_')[0]; + + for (const modal of modals) { + if (id == modal.name) { + modal.handler(client, interaction, ...interaction.customId.split('_').slice(1)); + return; + } + } + } else if (interaction.isAnySelectMenu()) { + // Handle menu selections. + const id = interaction.customId.split('_')[0]; + + for (const menu of menus) { + if (id == menu.name) { + menu.handler(client, interaction, ...interaction.customId.split('_').slice(1)); + return; + } + } + } +}); + +client.on('clientReady', async () => { + console.log(`Logged in as ${client.user!.tag}!`); + + await refreshCommands(client); + + await initIfNecessary(client, commands); + await initIfNecessary(client, buttons); + await initIfNecessary(client, modals); + await initIfNecessary(client, menus); + + await Database.open('./data/discord-main.db'); + await openRedisClient(config.REDIS_URL!, client); + + await initializeWebserver(client); + + ScheduledTasks.forEach((task) => scheduler.add(task)); + + ready = true; + console.log('Ready'); +}); + +client.on('guildAuditLogEntryCreate', handleAuditLogCreate); +client.on('guildBanRemove', handleBanRemove); +client.on('guildCreate', handleGuildCreate); +client.on('guildMemberAdd', handleMemberJoin); +client.on('messageCreate', handleMessageCreate); +client.on('messageDelete', handleMessageDelete); +client.on('messageDeleteBulk', handleBulkMessageDelete); +client.on('messageUpdate', handleMessageUpdate); +client.on('threadCreate', handleThreadCreate); +client.on('voiceStateUpdate', handleVoiceStateUpdate); + +client.on('error', console.error); +process.on('uncaughtException', console.error); + +client.login(config.DISCORD_TOKEN); diff --git a/src/scheduler/tasks/check-expired-bans-task.ts b/src/scheduler/tasks/check-expired-bans-task.ts index 9e353b9..836d1c6 100644 --- a/src/scheduler/tasks/check-expired-bans-task.ts +++ b/src/scheduler/tasks/check-expired-bans-task.ts @@ -11,4 +11,4 @@ class CheckExpiredBansTask implements Task { } } -export default new CheckExpiredBansTask(); \ No newline at end of file +export default new CheckExpiredBansTask(); diff --git a/src/scheduler/tasks/close-stale-tickets-task.ts b/src/scheduler/tasks/close-stale-tickets-task.ts index 8ff49e7..a566588 100644 --- a/src/scheduler/tasks/close-stale-tickets-task.ts +++ b/src/scheduler/tasks/close-stale-tickets-task.ts @@ -11,4 +11,4 @@ class CloseStaleTicketsTask implements Task { } } -export default new CloseStaleTicketsTask(); \ No newline at end of file +export default new CloseStaleTicketsTask(); diff --git a/src/scheduler/tasks/index.ts b/src/scheduler/tasks/index.ts index 77839ca..900da47 100644 --- a/src/scheduler/tasks/index.ts +++ b/src/scheduler/tasks/index.ts @@ -4,4 +4,4 @@ import closeStaleTicketsTask from './close-stale-tickets-task'; export default [ closeStaleTicketsTask, checkExpiredBansTask, -]; \ No newline at end of file +]; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 6cdbdc1..68026d8 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,6 +1,6 @@ -export * from './command.d'; -export * from './database-types.d'; -export * from './e621-types.d'; -export * from './handler.d'; -export * from './helper-types.d'; -export * from './scheduler.d'; +export * from './command.d'; +export * from './database-types.d'; +export * from './e621-types.d'; +export * from './handler.d'; +export * from './helper-types.d'; +export * from './scheduler.d'; diff --git a/src/types/scheduler.d.ts b/src/types/scheduler.d.ts index 10e28d1..e7223d4 100644 --- a/src/types/scheduler.d.ts +++ b/src/types/scheduler.d.ts @@ -5,4 +5,4 @@ export interface Task { firstRun: boolean; handle(context: Client): Promise; -}; \ No newline at end of file +};