diff --git a/src/api/routes/messages.ts b/src/api/routes/messages.ts index 241c093..3e70e18 100644 --- a/src/api/routes/messages.ts +++ b/src/api/routes/messages.ts @@ -2,12 +2,22 @@ import { Router, type Request } from 'express'; import { messageService } from '../../discord/services/index.js'; import { SendMessageSchema, EditMessageSchema, GetMessagesSchema } from '../../types/api.types.js'; +/** Route params for message endpoints (merged from parent router) */ +interface MessageParams { + channelId?: string; + messageId?: string; + emoji?: string; +} + const router = Router({ mergeParams: true }); -// Helper to get merged params +/** + * Helper to get merged params with type safety. + * Express mergeParams merges parent route params, so channelId comes from the parent router. + */ function getParams(req: Request): { channelId: string; messageId?: string; emoji?: string } { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const params = req.params as any; + // Params are merged from parent router via mergeParams: true + const params = req.params as MessageParams; return { channelId: params.channelId ?? '', messageId: params.messageId, emoji: params.emoji }; } diff --git a/src/discord/events/index.ts b/src/discord/events/index.ts index 17ec196..adff331 100644 --- a/src/discord/events/index.ts +++ b/src/discord/events/index.ts @@ -1,4 +1,5 @@ import type { Server as SocketIOServer } from 'socket.io'; +import type { User, GuildScheduledEvent } from 'discord.js'; import { discordClient } from '../client.js'; import { serializeMessage, @@ -267,7 +268,8 @@ export function registerDiscordEvents(): void { discordClient.on('userUpdate', (oldUser, newUser) => { // oldUser may be partial, so we only serialize if not partial - const oldSerialized = oldUser.partial ? null : serializeUser(oldUser as any); + // Type assertion is safe here because we've checked partial === false + const oldSerialized = oldUser.partial ? null : serializeUser(oldUser as User); broadcastEvent({ event: 'userUpdate', guildId: null, @@ -605,25 +607,40 @@ export function registerDiscordEvents(): void { discordClient.on('guildScheduledEventUpdate', (oldEvent, newEvent) => { // oldEvent may be partial + // Type assertions are safe here because we've checked partial === false const oldSerialized = oldEvent && !oldEvent.partial - ? serializeScheduledEvent(oldEvent as any) + ? serializeScheduledEvent(oldEvent as GuildScheduledEvent) : null; broadcastEvent({ event: 'guildScheduledEventUpdate', guildId: newEvent.guildId, data: { old: oldSerialized, - new: serializeScheduledEvent(newEvent as any), + new: serializeScheduledEvent(newEvent as GuildScheduledEvent), }, }); }); discordClient.on('guildScheduledEventDelete', (event) => { - // event may be partial, but we still want to broadcast the deletion + // For partial events, only broadcast minimal deletion info + // Full serialization would fail as many properties are undefined + if (event.partial) { + broadcastEvent({ + event: 'guildScheduledEventDelete', + guildId: event.guildId, + data: { + id: event.id, + guildId: event.guildId, + partial: true, + }, + }); + return; + } + broadcastEvent({ event: 'guildScheduledEventDelete', guildId: event.guildId, - data: serializeScheduledEvent(event as any), + data: serializeScheduledEvent(event as GuildScheduledEvent), }); }); diff --git a/src/index.ts b/src/index.ts index 61c7724..73ccbab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { loginDiscord, waitForReady, discordClient } from './discord/client.js'; import { registerDiscordEvents } from './discord/events/index.js'; import { createApiServer, startApiServer } from './api/server.js'; import { pluginManager } from './plugins/manager.js'; +import { shutdownRateLimiter } from './api/middleware/rateLimit.js'; import { config } from './config/index.js'; async function main(): Promise { @@ -44,6 +45,9 @@ async function main(): Promise { async function shutdown(): Promise { console.log('\nšŸ›‘ Shutting down...'); + // Clean up rate limiter intervals + shutdownRateLimiter(); + // Unload plugins gracefully if (pluginManager.count > 0) { console.log('šŸ”Œ Unloading plugins...'); diff --git a/src/plugins/manager.ts b/src/plugins/manager.ts index 746804e..2766e1b 100644 --- a/src/plugins/manager.ts +++ b/src/plugins/manager.ts @@ -214,7 +214,9 @@ export class PluginManager { normalizedArgs = args.map(wrapHandler); } - pluginSubRouter!.use(...normalizedArgs as Parameters); + // pluginSubRouter is guaranteed non-null here since we just created it + const router = pluginSubRouter as Router; + router.use(...normalizedArgs as Parameters); }, }; diff --git a/src/plugins/sdk.ts b/src/plugins/sdk.ts index ebb237f..2571cb1 100644 --- a/src/plugins/sdk.ts +++ b/src/plugins/sdk.ts @@ -167,6 +167,11 @@ export function createEventHelpers(eventBus: PluginEventBus) { }; } +/** + * Type for the event helpers object returned by createEventHelpers + */ +export type EventHelpers = ReturnType; + /** * Plugin definition options for definePlugin helper */ diff --git a/src/types/events.types.ts b/src/types/events.types.ts index c07a629..90f8d7b 100644 --- a/src/types/events.types.ts +++ b/src/types/events.types.ts @@ -563,7 +563,14 @@ export interface GuildScheduledEventUpdateEvent { export interface GuildScheduledEventDeleteEvent { event: 'guildScheduledEventDelete'; guildId: string; - data: SerializedScheduledEvent; + data: SerializedScheduledEvent | { + /** Event ID (always available even for partial events) */ + id: string; + /** Guild ID (always available even for partial events) */ + guildId: string; + /** Indicates this is a partial event with limited data */ + partial: true; + }; } export interface GuildScheduledEventUserAddEvent { diff --git a/src/types/plugin.types.ts b/src/types/plugin.types.ts index 06132fb..cf2b335 100644 --- a/src/types/plugin.types.ts +++ b/src/types/plugin.types.ts @@ -9,7 +9,7 @@ import type { SocketData, } from './events.types.js'; import type { PluginEventBus, EventSubscription } from '../plugins/event-bus.js'; -import type { PluginRouter, PluginLogger } from '../plugins/sdk.js'; +import type { PluginRouter, PluginLogger, EventHelpers } from '../plugins/sdk.js'; /** * The context passed to plugins on load. @@ -92,13 +92,7 @@ export interface HoloPlugin { * ``` */ events?: ( - helpers: { - onDiscord: (event: string, handler: (data: T) => void | Promise) => EventSubscription; - onCustom: >(event: string, handler: (data: T) => void | Promise) => EventSubscription; - emit: >(event: string, data: T) => void; - onPluginLoaded: (handler: (data: { name: string; version: string }) => void) => EventSubscription; - onPluginUnloaded: (handler: (data: { name: string }) => void) => EventSubscription; - }, + helpers: EventHelpers, ctx: PluginContext ) => EventSubscription[];