feat(events): Adds AcknowledgableEvent

This commit is contained in:
Michel Fedde 2025-06-22 15:51:14 +02:00
parent 6d7a0e7cfb
commit b82ab7dbc4
18 changed files with 228 additions and 77 deletions

View file

@ -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,

View file

@ -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;
}

View file

@ -1,10 +0,0 @@
import {AnySelectMenuInteraction, ButtonInteraction} from "discord.js";
export class ComponentInteractionEvent {
constructor(
public readonly interaction: AnySelectMenuInteraction | ButtonInteraction
) {
}
}

View file

@ -1,18 +1,22 @@
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>(EventHandler.name);
const events: TimedEvent[] = [
new ReminderEvent(),
new CleanupEvent(eventHandler, Container.get<Logger>("logger"))
]
events.forEach((event) => {
eventHandler.addTimed(event);
})
@ -21,6 +25,9 @@ export class DefaultEvents {
public static setupHandlers() {
const eventHandler = Container.get<EventHandler>(EventHandler.name);
eventHandler.addHandler<ElementCreatedEvent<PlaydateModel>>(ElementCreatedEvent.name, sendCreatedNotificationEventHandler);
eventHandler.addHandler<ElementCreatedEvent<PlaydateModel>>(ElementCreatedEvent.name, {
method: sendCreatedNotificationEventHandler,
persistent: true
});
}
}

View file

@ -1,10 +0,0 @@
import {Model} from "../Models/Model";
export class ElementCreatedEvent<T extends Model = Model> {
constructor(
public readonly tableName: string,
public readonly instanceValues: Partial<T>,
public readonly instanceId: number | bigint
) {
}
}

View file

@ -0,0 +1,5 @@
import {EventType, NormalEvent} from "../EventHandler.types";
export class CleanEvent implements NormalEvent {
type: EventType.Normal = EventType.Normal;
}

View file

@ -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: []
})
}
}

View file

@ -0,0 +1,13 @@
import {Model} from "../../Models/Model";
import {EventType, NormalEvent} from "../EventHandler.types";
export class ElementCreatedEvent<T extends Model = Model> implements NormalEvent {
constructor(
public readonly tableName: string,
public readonly instanceValues: Partial<T>,
public readonly instanceId: number | bigint
) {
}
type: EventType.Normal = EventType.Normal;
}

View file

@ -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
) {
}
}

View file

@ -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<string, Map<string, CallableFunction>> = new Map();
private static DEFAULT_EVENT_ENTRY: Omit<EventEntry, "method"> = {
persistent: false
}
private eventHandlers: Map<string, Map<string, EventEntry>> = new Map();
public get EventHandlers() {
return this.eventHandlers;
}
constructor() {
}
public addHandler<T extends Class>(eventName: string, handler: (event: T) => void): string {
public addHandler<T extends Class>(eventName: string, entry: EventEntry<T>): 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<T extends Class>(event: T) {
public dispatch<T extends HandlerEvents>(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>("logger").error(e);
}
})
if (event.type !== EventType.Acknowledge) {
return;
}
if (event.isAcknowledged()) {
return;
}
event.handleUnacknowledgement();
}
public addTimed(event: TimedEvent) {

View file

@ -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<T = any> = {
method: (event: T) => void,
persistent?: boolean
}

View file

@ -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<CleanEvent>(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);
}
}
}
}

View file

@ -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>(DiscordClient.name);
}
async execute() {
const today = new Date();
today.setHours(0, 0, 0, 0);

View file

@ -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";

View file

@ -1,8 +0,0 @@
import {ModalSubmitInteraction} from "discord.js";
export class ModalInteractionEvent {
constructor(
public readonly interaction: ModalSubmitInteraction
) {}
}

View file

@ -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>(ComponentInteractionEvent.name, this.handleUIEvents.bind(this));
this.eventId = this.eventHandler?.addHandler<ComponentInteractionEvent>(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<ButtonBuilder>()
.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 = <MenuAction>action;

View file

@ -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<ModalSubmitInteraction>
{
return new Promise<ModalSubmitInteraction>((resolve) => {
this.eventHandler.addHandler<ModalInteractionEvent>(ModalInteractionEvent.name, (ev) => {
this.eventHandler.addHandler<ModalInteractionEvent>(ModalInteractionEvent.name, {
method: (ev) => {
if (this.modalId !== ev.interaction.customId) {
return;
}
resolve(ev.interaction);
},
persistent: false
})
})
}

View file

@ -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<ModelType extends Model, IntermediateModelType = unknown> {