diff --git a/source/Discord/InteractionRouter.ts b/source/Discord/InteractionRouter.ts index 4e48d16..74b91a5 100644 --- a/source/Discord/InteractionRouter.ts +++ b/source/Discord/InteractionRouter.ts @@ -11,8 +11,8 @@ import {Logger} from "log4js"; import {UserError} from "./UserError"; import {Container} from "../Container/Container"; import {EventHandler} from "../Events/EventHandler"; -import {ModalInteractionEvent} from "../Events/ModalInteractionEvent"; -import {ComponentInteractionEvent} from "../Events/ComponentInteractionEvent"; +import {ModalInteractionEvent} from "../Events/EventClasses/ModalInteractionEvent"; +import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent"; enum InteractionRoutingType { Unrouted, diff --git a/source/Events/AcknowledgableEvent.ts b/source/Events/AcknowledgableEvent.ts new file mode 100644 index 0000000..4b0affb --- /dev/null +++ b/source/Events/AcknowledgableEvent.ts @@ -0,0 +1,20 @@ +import {AcknowledgeEvent, BaseEvent, EventType} from "./EventHandler.types"; + +export abstract class AcknowledgableEvent implements AcknowledgeEvent { + type: EventType.Acknowledge = EventType.Acknowledge; + + private acknowledged: boolean = false; + + public isAcknowledged(): boolean { + return this.acknowledged; + } + + /** + * This is supposed to be executed by the method called by the event. + */ + public acknowledge() { + this.acknowledged = true; + } + + public abstract handleUnacknowledgement(): void; +} \ No newline at end of file diff --git a/source/Events/ComponentInteractionEvent.ts b/source/Events/ComponentInteractionEvent.ts deleted file mode 100644 index c4a3f71..0000000 --- a/source/Events/ComponentInteractionEvent.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {AnySelectMenuInteraction, ButtonInteraction} from "discord.js"; - -export class ComponentInteractionEvent { - - constructor( - public readonly interaction: AnySelectMenuInteraction | ButtonInteraction - ) { - } - -} \ No newline at end of file diff --git a/source/Events/DefaultEvents.ts b/source/Events/DefaultEvents.ts index f1c6924..7939fc1 100644 --- a/source/Events/DefaultEvents.ts +++ b/source/Events/DefaultEvents.ts @@ -1,17 +1,21 @@ -import {EventHandler, TimedEvent} from "./EventHandler"; +import {EventHandler, } from "./EventHandler"; import {Container} from "../Container/Container"; -import {ReminderEvent} from "./ReminderEvent"; -import {ElementCreatedEvent} from "./ElementCreatedEvent"; +import {ReminderEvent} from "./Handlers/ReminderEvent"; +import {ElementCreatedEvent} from "./EventClasses/ElementCreatedEvent"; import {sendCreatedNotificationEventHandler} from "./Handlers/SendCreatedNotification"; import {PlaydateModel} from "../Models/PlaydateModel"; +import {TimedEvent} from "./EventHandler.types"; +import {CleanupEvent} from "./Handlers/CleanupEvent"; +import {Logger} from "log4js"; export class DefaultEvents { public static setupTimed() { - const events: TimedEvent[] = [ - new ReminderEvent() - ] - const eventHandler = Container.get(EventHandler.name); + + const events: TimedEvent[] = [ + new ReminderEvent(), + new CleanupEvent(eventHandler, Container.get("logger")) + ] events.forEach((event) => { eventHandler.addTimed(event); @@ -21,6 +25,9 @@ export class DefaultEvents { public static setupHandlers() { const eventHandler = Container.get(EventHandler.name); - eventHandler.addHandler>(ElementCreatedEvent.name, sendCreatedNotificationEventHandler); + eventHandler.addHandler>(ElementCreatedEvent.name, { + method: sendCreatedNotificationEventHandler, + persistent: true + }); } } \ No newline at end of file diff --git a/source/Events/ElementCreatedEvent.ts b/source/Events/ElementCreatedEvent.ts deleted file mode 100644 index c09af5f..0000000 --- a/source/Events/ElementCreatedEvent.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Model} from "../Models/Model"; - -export class ElementCreatedEvent { - constructor( - public readonly tableName: string, - public readonly instanceValues: Partial, - public readonly instanceId: number | bigint - ) { - } -} \ No newline at end of file diff --git a/source/Events/EventClasses/CleanEvent.ts b/source/Events/EventClasses/CleanEvent.ts new file mode 100644 index 0000000..8b9e794 --- /dev/null +++ b/source/Events/EventClasses/CleanEvent.ts @@ -0,0 +1,5 @@ +import {EventType, NormalEvent} from "../EventHandler.types"; + +export class CleanEvent implements NormalEvent { + type: EventType.Normal = EventType.Normal; +} \ No newline at end of file diff --git a/source/Events/EventClasses/ComponentInteractionEvent.ts b/source/Events/EventClasses/ComponentInteractionEvent.ts new file mode 100644 index 0000000..c0d8ad4 --- /dev/null +++ b/source/Events/EventClasses/ComponentInteractionEvent.ts @@ -0,0 +1,21 @@ +import {AnySelectMenuInteraction, ButtonInteraction, InteractionCallbackResponse} from "discord.js"; +import {EventType, NormalEvent} from "../EventHandler.types"; +import {AcknowledgableEvent} from "../AcknowledgableEvent"; + +export class ComponentInteractionEvent extends AcknowledgableEvent { + + constructor( + public readonly interaction: AnySelectMenuInteraction | ButtonInteraction + ) { + super(); + } + + public handleUnacknowledgement() { + this.interaction.update({ + content: ":x: Interaction not longer available. Please restart the process.", + embeds: [], + components: [] + }) + } + +} \ No newline at end of file diff --git a/source/Events/EventClasses/ElementCreatedEvent.ts b/source/Events/EventClasses/ElementCreatedEvent.ts new file mode 100644 index 0000000..33e33ef --- /dev/null +++ b/source/Events/EventClasses/ElementCreatedEvent.ts @@ -0,0 +1,13 @@ +import {Model} from "../../Models/Model"; +import {EventType, NormalEvent} from "../EventHandler.types"; + +export class ElementCreatedEvent implements NormalEvent { + constructor( + public readonly tableName: string, + public readonly instanceValues: Partial, + public readonly instanceId: number | bigint + ) { + } + + type: EventType.Normal = EventType.Normal; +} \ No newline at end of file diff --git a/source/Events/EventClasses/ModalInteractionEvent.ts b/source/Events/EventClasses/ModalInteractionEvent.ts new file mode 100644 index 0000000..03d1566 --- /dev/null +++ b/source/Events/EventClasses/ModalInteractionEvent.ts @@ -0,0 +1,14 @@ +import {ModalSubmitInteraction} from "discord.js"; +import {EventType, NormalEvent} from "../EventHandler.types"; + +export class ModalInteractionEvent implements NormalEvent { + + type: EventType.Normal = EventType.Normal; + + constructor( + public readonly interaction: ModalSubmitInteraction + ) { + } + + +} \ No newline at end of file diff --git a/source/Events/EventHandler.ts b/source/Events/EventHandler.ts index dd41dc5..0313fa2 100644 --- a/source/Events/EventHandler.ts +++ b/source/Events/EventHandler.ts @@ -1,33 +1,34 @@ -import cron, {validate} from "node-cron"; +import cron from "node-cron"; import {Class} from "../types/Class"; import {randomUUID} from "node:crypto"; import {Container} from "../Container/Container"; import {Logger} from "log4js"; - -export type EventConfiguration = { - name: string, - maxExecutions?: number, -} - -export interface TimedEvent { - configuration: EventConfiguration, - cronExpression: string, - execute: () => void -} +import {BaseEvent, EventEntry, EventType, HandlerEvents, TimedEvent} from "./EventHandler.types"; export class EventHandler { - private eventHandlers: Map> = new Map(); + private static DEFAULT_EVENT_ENTRY: Omit = { + persistent: false + } + + private eventHandlers: Map> = new Map(); + + public get EventHandlers() { + return this.eventHandlers; + } constructor() { } - public addHandler(eventName: string, handler: (event: T) => void): string { + public addHandler(eventName: string, entry: EventEntry): string { if (!this.eventHandlers.has(eventName)) { this.eventHandlers.set(eventName, new Map()); } const id = randomUUID(); - this.eventHandlers.get(eventName)?.set(id, handler); + this.eventHandlers.get(eventName)?.set(id, { + ...EventHandler.DEFAULT_EVENT_ENTRY, + ...entry + }); return id; } @@ -44,7 +45,7 @@ export class EventHandler { localEventHandlers.delete(id); } - public dispatch(event: T) { + public dispatch(event: T) { const eventName = event.constructor.name; if (!this.eventHandlers.has(eventName)) { return; @@ -52,11 +53,21 @@ export class EventHandler { this.eventHandlers.get(eventName)?.forEach((handler) => { try { - handler(event); + handler.method(event); } catch (e: any) { Container.get("logger").error(e); } }) + + if (event.type !== EventType.Acknowledge) { + return; + } + + if (event.isAcknowledged()) { + return; + } + + event.handleUnacknowledgement(); } public addTimed(event: TimedEvent) { diff --git a/source/Events/EventHandler.types.ts b/source/Events/EventHandler.types.ts new file mode 100644 index 0000000..577d3d6 --- /dev/null +++ b/source/Events/EventHandler.types.ts @@ -0,0 +1,40 @@ +import {AcknowledgableEvent} from "./AcknowledgableEvent"; + +export enum EventType { + Normal, + TimeBased, + Acknowledge, +} + +export interface BaseEvent { + type: EventType +} +export interface NormalEvent extends BaseEvent { + type: EventType.Normal +} + +export interface AcknowledgeEvent extends BaseEvent { + type: EventType.Acknowledge, + isAcknowledged(): boolean, + handleUnacknowledgement(): void +} + +export type HandlerEvents = NormalEvent | AcknowledgeEvent + +export interface TimedEvent extends BaseEvent { + type: EventType.TimeBased, + configuration: EventConfiguration, + cronExpression: string, + execute: () => void +} + + +export type EventConfiguration = { + name: string, + maxExecutions?: number, +} + +export type EventEntry = { + method: (event: T) => void, + persistent?: boolean +} diff --git a/source/Events/Handlers/CleanupEvent.ts b/source/Events/Handlers/CleanupEvent.ts new file mode 100644 index 0000000..d7b174b --- /dev/null +++ b/source/Events/Handlers/CleanupEvent.ts @@ -0,0 +1,38 @@ +import {EventConfiguration, EventType, TimedEvent} from "../EventHandler.types"; +import {EventHandler} from "../EventHandler"; +import {Logger} from "log4js"; +import {CleanEvent} from "../EventClasses/CleanEvent"; + +export class CleanupEvent implements TimedEvent { + type: EventType.TimeBased = EventType.TimeBased; + configuration: EventConfiguration = { + name: "Cleanup event", + }; + cronExpression: string = "0 5 * * *"; + + constructor( + private readonly eventHandler: EventHandler, + private readonly logger: Logger + ) {} + + public execute(): void { + this.logger.info("Issue cleanup...") + + this.eventHandler.dispatch(new CleanEvent()); + this.cleanEvents(); + } + + private cleanEvents(): void { + const eventHandlers = this.eventHandler.EventHandlers; + for (let [eventClass, entries] of eventHandlers.entries()) { + for (let [id, entry] of entries.entries()) { + if (entry.persistent) { + continue; + } + + this.eventHandler.removeHandler(eventClass, id); + } + } + } + +} \ No newline at end of file diff --git a/source/Events/ReminderEvent.ts b/source/Events/Handlers/ReminderEvent.ts similarity index 82% rename from source/Events/ReminderEvent.ts rename to source/Events/Handlers/ReminderEvent.ts index 3fd740f..bf17f86 100644 --- a/source/Events/ReminderEvent.ts +++ b/source/Events/Handlers/ReminderEvent.ts @@ -1,13 +1,13 @@ -import {EventConfiguration, TimedEvent} from "./EventHandler"; -import {Container} from "../Container/Container"; -import {PlaydateRepository} from "../Repositories/PlaydateRepository"; -import {GroupConfigurationHandler} from "../Groups/GroupConfigurationHandler"; -import {GroupConfigurationRepository} from "../Repositories/GroupConfigurationRepository"; -import {PlaydateModel} from "../Models/PlaydateModel"; -import {ChannelId} from "../types/DiscordTypes"; -import {DiscordClient} from "../Discord/DiscordClient"; +import {Container} from "../../Container/Container"; +import {PlaydateRepository} from "../../Repositories/PlaydateRepository"; +import {GroupConfigurationHandler} from "../../Groups/GroupConfigurationHandler"; +import {GroupConfigurationRepository} from "../../Repositories/GroupConfigurationRepository"; +import {PlaydateModel} from "../../Models/PlaydateModel"; +import {ChannelId} from "../../types/DiscordTypes"; +import {DiscordClient} from "../../Discord/DiscordClient"; import {EmbedBuilder, roleMention, time} from "discord.js"; -import {ArrayUtils} from "../Utilities/ArrayUtils"; +import {ArrayUtils} from "../../Utilities/ArrayUtils"; +import {EventConfiguration, EventType, TimedEvent} from "../EventHandler.types"; export class ReminderEvent implements TimedEvent { private static REMINDER_INTERVALS = [ @@ -22,6 +22,7 @@ export class ReminderEvent implements TimedEvent { ] + type: EventType.TimeBased = EventType.TimeBased; configuration: EventConfiguration = { name: "Reminders", } @@ -38,6 +39,7 @@ export class ReminderEvent implements TimedEvent { this.discordClient = Container.get(DiscordClient.name); } + async execute() { const today = new Date(); today.setHours(0, 0, 0, 0); diff --git a/source/Events/Handlers/SendCreatedNotification.ts b/source/Events/Handlers/SendCreatedNotification.ts index 84266b5..d1700e9 100644 --- a/source/Events/Handlers/SendCreatedNotification.ts +++ b/source/Events/Handlers/SendCreatedNotification.ts @@ -1,4 +1,4 @@ -import {ElementCreatedEvent} from "../ElementCreatedEvent"; +import {ElementCreatedEvent} from "../EventClasses/ElementCreatedEvent"; import {PlaydateModel} from "../../Models/PlaydateModel"; import PlaydateTableConfiguration from "../../Database/tables/Playdate"; import {EmbedBuilder, roleMention, time} from "discord.js"; diff --git a/source/Events/ModalInteractionEvent.ts b/source/Events/ModalInteractionEvent.ts deleted file mode 100644 index e92801d..0000000 --- a/source/Events/ModalInteractionEvent.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {ModalSubmitInteraction} from "discord.js"; - -export class ModalInteractionEvent { - constructor( - public readonly interaction: ModalSubmitInteraction - ) {} - -} \ No newline at end of file diff --git a/source/Menu/MenuRenderer.ts b/source/Menu/MenuRenderer.ts index 96a16be..fa1e77d 100644 --- a/source/Menu/MenuRenderer.ts +++ b/source/Menu/MenuRenderer.ts @@ -14,7 +14,7 @@ import { import {Nullable} from "../types/Nullable"; import {IconCache} from "../Icons/IconCache"; import {MessageActionRowComponentBuilder} from "@discordjs/builders"; -import {ComponentInteractionEvent} from "../Events/ComponentInteractionEvent"; +import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent"; import {MenuTraversal} from "./MenuTraversal"; import {Prompt} from "./Modals/Prompt"; @@ -52,7 +52,9 @@ export class MenuRenderer { } public async display(interaction: CommandInteraction) { - this.eventId = this.eventHandler?.addHandler(ComponentInteractionEvent.name, this.handleUIEvents.bind(this)); + this.eventId = this.eventHandler?.addHandler(ComponentInteractionEvent.name, { + method: this.handleUIEvents.bind(this), + }); await interaction.reply({ content: "", @@ -91,12 +93,13 @@ export class MenuRenderer { ); } - return Array.from(Array(rowCount).keys()) + const rows = Array.from(Array(rowCount).keys()) .map((index) => { const childStart = index * MenuRenderer.MAX_BUTTON_PER_ROW; - return menuItem.children.toSpliced(childStart, MenuRenderer.MAX_BUTTON_PER_ROW); + return menuItem.children.slice(childStart, MenuRenderer.MAX_BUTTON_PER_ROW); }) - .reverse() + + return rows.reverse() .map((items) => new ActionRowBuilder() .setComponents( ...items.map(item => new ButtonBuilder() @@ -150,6 +153,8 @@ export class MenuRenderer { return; } + ev.acknowledge(); + const [, action, parameter ] = ev.interaction.customId.split(';') const menuAction = action; diff --git a/source/Menu/Modals/Modal.ts b/source/Menu/Modals/Modal.ts index 50f89a4..aa74dd3 100644 --- a/source/Menu/Modals/Modal.ts +++ b/source/Menu/Modals/Modal.ts @@ -1,7 +1,7 @@ import {EventHandler} from "../../Events/EventHandler"; import {randomUUID} from "node:crypto"; import {ModalBuilder, ModalSubmitInteraction} from "discord.js"; -import {ModalInteractionEvent} from "../../Events/ModalInteractionEvent"; +import {ModalInteractionEvent} from "../../Events/EventClasses/ModalInteractionEvent"; export abstract class Modal { private readonly modalId: string; @@ -19,11 +19,14 @@ export abstract class Modal { protected awaitResponse(): Promise { return new Promise((resolve) => { - this.eventHandler.addHandler(ModalInteractionEvent.name, (ev) => { - if (this.modalId !== ev.interaction.customId) { - return; - } - resolve(ev.interaction); + this.eventHandler.addHandler(ModalInteractionEvent.name, { + method: (ev) => { + if (this.modalId !== ev.interaction.customId) { + return; + } + resolve(ev.interaction); + }, + persistent: false }) }) } diff --git a/source/Repositories/Repository.ts b/source/Repositories/Repository.ts index aa73139..d8c2552 100644 --- a/source/Repositories/Repository.ts +++ b/source/Repositories/Repository.ts @@ -4,7 +4,7 @@ import {Nullable} from "../types/Nullable"; import {DatabaseDefinition} from "../Database/DatabaseDefinition"; import {Container} from "../Container/Container"; import {EventHandler} from "../Events/EventHandler"; -import {ElementCreatedEvent} from "../Events/ElementCreatedEvent"; +import {ElementCreatedEvent} from "../Events/EventClasses/ElementCreatedEvent"; export class Repository {