From e55886cd8fb1def691411a67c13fcbcea6ddd954 Mon Sep 17 00:00:00 2001 From: a Date: Sat, 14 Jun 2025 22:32:19 -0500 Subject: [PATCH] noot --- ts/src/activities/database.ts | 2 +- ts/src/activities/guild_messages.ts | 16 +- ts/src/activities/players.ts | 2 - ts/src/cmd/worker.ts | 4 +- .../discord/botevent/command_parser.spec.ts | 299 ++++++------------ ts/src/discord/botevent/command_parser.ts | 82 ++--- ts/src/discord/botevent/slash_commands.ts | 2 +- ts/src/discord/index.ts | 6 +- ts/src/discord/mux/index.ts | 1 - ts/src/lib/util/tabwriter.ts | 6 +- ts/src/lib/wynn/wapi.ts | 2 +- ts/src/payload_converter/adapter.ts | 2 +- ts/src/workflows/discord.ts | 4 +- 13 files changed, 164 insertions(+), 264 deletions(-) diff --git a/ts/src/activities/database.ts b/ts/src/activities/database.ts index 1716e2a..ca4210b 100644 --- a/ts/src/activities/database.ts +++ b/ts/src/activities/database.ts @@ -31,7 +31,7 @@ export async function update_wynn_items() { return } found_new = true - log.info(`updating wynn with new hash`, { old: currenthash, new: dataHash }) + log.info('updating wynn with new hash', { old: currenthash, new: dataHash }) for (const [displayName, item] of Object.entries(parsed)) { const json = stringify(item) if (!json) { diff --git a/ts/src/activities/guild_messages.ts b/ts/src/activities/guild_messages.ts index 315a9ea..cd53e6d 100644 --- a/ts/src/activities/guild_messages.ts +++ b/ts/src/activities/guild_messages.ts @@ -27,7 +27,7 @@ select * from ranked where ranked.uid = ${guild_id} ` - if (result.length == 0) { + if (result.length === 0) { return { content: 'No guild found.', } @@ -36,9 +36,9 @@ where ranked.uid = ${guild_id} const guild = result[0] const output = [ - `# 🏰 Guild Information`, + '# 🏰 Guild Information', `## **[${guild.prefix}] ${guild.name}**\n`, - `### 📊 Statistics`, + '### 📊 Statistics', `> **Level:** \`${guild.level}\``, `> **Total XP:** \`${formatNumber(guild.xp)}\``, `> **XP Rank:** \`#${guild.xp_rank >= 1000 ? '1000+' : guild.xp_rank}\``, @@ -81,7 +81,7 @@ export async function formGuildOnlineMessage(guild_id: string): Promise { - if (acc[member.server] == undefined) { + if (acc[member.server] === undefined) { acc[member.server] = [] } acc[member.server].push(member) @@ -122,7 +122,7 @@ export async function formGuildOnlineMessage(guild_id: string): Promise { log.warn(`failed to get uuid for ${playerName}`, { err: e, }) - continue } } } @@ -113,7 +112,6 @@ export const scrape_online_players = async () => { log.warn(`failed to update server for ${playerName}`, { err: e, }) - continue } } }) diff --git a/ts/src/cmd/worker.ts b/ts/src/cmd/worker.ts index 2f27cbe..6652d15 100644 --- a/ts/src/cmd/worker.ts +++ b/ts/src/cmd/worker.ts @@ -4,7 +4,7 @@ import { c } from '#/di' // di import '#/services/temporal' -import path from 'path' +import path from 'node:path' import { Client, ScheduleNotFoundError, type ScheduleOptions, ScheduleOverlapPolicy } from '@temporalio/client' import { NativeConnection, Worker } from '@temporalio/worker' import { PG } from '#/services/pg' @@ -129,7 +129,7 @@ export class WorkerCommand extends Command { if (!config.resolve.alias) config.resolve.alias = {} config.resolve.alias = { '#': path.resolve(process.cwd(), 'src/'), - ...config.resolve!.alias, + ...config.resolve?.alias, } return config }, diff --git a/ts/src/discord/botevent/command_parser.spec.ts b/ts/src/discord/botevent/command_parser.spec.ts index 13a1245..dbec695 100644 --- a/ts/src/discord/botevent/command_parser.spec.ts +++ b/ts/src/discord/botevent/command_parser.spec.ts @@ -111,27 +111,44 @@ const TEST_COMMANDS = [ }, ] as const satisfies CreateApplicationCommand[] +// Helper function to create a mock handler structure with all required commands +function createMockHandlers() { + return { + simple: vi.fn(), + complex: { + list: vi.fn(), + create: vi.fn(), + }, + admin: { + user: { + ban: vi.fn(), + kick: vi.fn(), + }, + }, + } +} + +// Helper function to create a complete test setup +function createTestSetup() { + const handlers = createMockHandlers() + const notFoundHandler = vi.fn() + + const handler = createCommandHandler({ + commands: TEST_COMMANDS, + handler: handlers, + notFoundHandler, + }) + + return { + handlers, + notFoundHandler, + handler, + } +} + describe('createCommandHandler', () => { it('should handle a simple command with required string argument', async () => { - const simpleHandler = vi.fn() - const notFoundHandler = vi.fn() - - const handler = createCommandHandler({ - handler: { - simple: simpleHandler, - complex: { - list: vi.fn(), - create: vi.fn(), - }, - admin: { - user: { - ban: vi.fn(), - kick: vi.fn(), - }, - }, - }, - notFoundHandler, - }) + const { handlers, notFoundHandler, handler } = createTestSetup() const interactionData: InteractionData = { name: 'simple', @@ -140,31 +157,12 @@ describe('createCommandHandler', () => { await handler(interactionData) - expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello world' }) + expect(handlers.simple).toHaveBeenCalledWith({ message: 'Hello world' }) expect(notFoundHandler).not.toHaveBeenCalled() }) it('should handle a simple command with optional arguments', async () => { - const simpleHandler = vi.fn() - const notFoundHandler = vi.fn() - let a: ExtractCommands - - const handler = createCommandHandler({ - handler: { - simple: simpleHandler, - complex: { - list: vi.fn(), - create: vi.fn(), - }, - admin: { - user: { - ban: vi.fn(), - kick: vi.fn(), - }, - }, - }, - notFoundHandler, - }) + const { handlers, handler } = createTestSetup() const interactionData: InteractionData = { name: 'simple', @@ -176,30 +174,11 @@ describe('createCommandHandler', () => { await handler(interactionData) - expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello', count: 5 }) + expect(handlers.simple).toHaveBeenCalledWith({ message: 'Hello', count: 5 }) }) it('should handle subcommands correctly', async () => { - const listHandler = vi.fn() - const createHandler = vi.fn() - const notFoundHandler = vi.fn() - - const handler = createCommandHandler({ - handler: { - simple: vi.fn(), - complex: { - list: listHandler, - create: createHandler, - }, - admin: { - user: { - ban: vi.fn(), - kick: vi.fn(), - }, - }, - }, - notFoundHandler, - }) + const { handlers, notFoundHandler, handler } = createTestSetup() const interactionData: InteractionData = { name: 'complex', @@ -217,30 +196,13 @@ describe('createCommandHandler', () => { await handler(interactionData) - expect(createHandler).toHaveBeenCalledWith({ name: 'Test Item', enabled: true }) - expect(listHandler).not.toHaveBeenCalled() + expect(handlers.complex.create).toHaveBeenCalledWith({ name: 'Test Item', enabled: true }) + expect(handlers.complex.list).not.toHaveBeenCalled() expect(notFoundHandler).not.toHaveBeenCalled() }) it('should call notFoundHandler for unknown commands', async () => { - const notFoundHandler = vi.fn() - - const handler = createCommandHandler({ - handler: { - simple: vi.fn(), - complex: { - list: vi.fn(), - create: vi.fn(), - }, - admin: { - user: { - ban: vi.fn(), - kick: vi.fn(), - }, - }, - }, - notFoundHandler, - }) + const { notFoundHandler, handler } = createTestSetup() const interactionData: InteractionData = { name: 'unknown', @@ -253,24 +215,7 @@ describe('createCommandHandler', () => { }) it('should call notFoundHandler for unknown subcommands', async () => { - const notFoundHandler = vi.fn() - - const handler = createCommandHandler({ - handler: { - simple: vi.fn(), - complex: { - list: vi.fn(), - create: vi.fn(), - }, - admin: { - user: { - ban: vi.fn(), - kick: vi.fn(), - }, - }, - }, - notFoundHandler, - }) + const { notFoundHandler, handler } = createTestSetup() const interactionData: InteractionData = { name: 'complex', @@ -289,24 +234,7 @@ describe('createCommandHandler', () => { }) it('should handle missing interaction data', async () => { - const notFoundHandler = vi.fn() - - const handler = createCommandHandler({ - handler: { - simple: vi.fn(), - complex: { - list: vi.fn(), - create: vi.fn(), - }, - admin: { - user: { - ban: vi.fn(), - kick: vi.fn(), - }, - }, - }, - notFoundHandler, - }) + const { notFoundHandler, handler } = createTestSetup() await handler(null as any) expect(notFoundHandler).toHaveBeenCalledWith({}) @@ -316,26 +244,7 @@ describe('createCommandHandler', () => { }) it('should handle subcommand groups recursively', async () => { - const banHandler = vi.fn() - const kickHandler = vi.fn() - const notFoundHandler = vi.fn() - - const handler = createCommandHandler({ - handler: { - simple: vi.fn(), - complex: { - list: vi.fn(), - create: vi.fn(), - }, - admin: { - user: { - ban: banHandler, - kick: kickHandler, - }, - }, - }, - notFoundHandler, - }) + const { handlers, notFoundHandler, handler } = createTestSetup() const interactionData: InteractionData = { name: 'admin', @@ -359,30 +268,13 @@ describe('createCommandHandler', () => { await handler(interactionData) - expect(banHandler).toHaveBeenCalledWith({ user: '123456789', reason: 'Spam' }) - expect(kickHandler).not.toHaveBeenCalled() + expect(handlers.admin.user.ban).toHaveBeenCalledWith({ user: '123456789', reason: 'Spam' }) + expect(handlers.admin.user.kick).not.toHaveBeenCalled() expect(notFoundHandler).not.toHaveBeenCalled() }) it('should handle commands without options', async () => { - const simpleHandler = vi.fn() - - const handler = createCommandHandler({ - handler: { - simple: simpleHandler, - complex: { - list: vi.fn(), - create: vi.fn(), - }, - admin: { - user: { - ban: vi.fn(), - kick: vi.fn(), - }, - }, - }, - notFoundHandler: vi.fn(), - }) + const { handlers, handler } = createTestSetup() const interactionData: InteractionData = { name: 'simple', @@ -391,28 +283,11 @@ describe('createCommandHandler', () => { await handler(interactionData) - expect(simpleHandler).toHaveBeenCalledWith({}) + expect(handlers.simple).toHaveBeenCalledWith({}) }) it('should ignore subcommand options when parsing top-level args', async () => { - const simpleHandler = vi.fn() - - const handler = createCommandHandler({ - handler: { - simple: simpleHandler, - complex: { - list: vi.fn(), - create: vi.fn(), - }, - admin: { - user: { - ban: vi.fn(), - kick: vi.fn(), - }, - }, - }, - notFoundHandler: vi.fn(), - }) + const { handlers, handler } = createTestSetup() const interactionData: InteractionData = { name: 'simple', @@ -425,7 +300,7 @@ describe('createCommandHandler', () => { await handler(interactionData) - expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello' }) + expect(handlers.simple).toHaveBeenCalledWith({ message: 'Hello' }) }) }) @@ -482,6 +357,7 @@ describe('ExtractCommands type utility', () => { it('should infer types correctly without manual typing', () => { // This test verifies that args are inferred correctly without manual type annotations const handler = createCommandHandler({ + commands: TEST_COMMANDS, notFoundHandler: async () => {}, handler: { simple: async (args) => { @@ -551,7 +427,9 @@ describe('ExtractCommands type utility', () => { expect(handlers).toBeDefined() }) - it('should handle different option types', () => { + it('should handle different option types', async () => { + const { type } = await import('arktype') + const TYPE_TEST_COMMANDS = [ { name: 'types', @@ -573,31 +451,54 @@ describe('ExtractCommands type utility', () => { type TypeHandlers = ExtractCommands + // Define arktype validators for each expected type + const argsValidator = type({ + str: 'string', + int: 'number.integer', + bool: 'boolean', + user: 'string', // User IDs are strings + channel: 'string', // Channel IDs are strings + role: 'string', // Role IDs are strings + num: 'number', + mention: 'string', // Mentionable IDs are strings + attach: 'string', // Attachment IDs are strings + }) + const handlers: TypeHandlers = { types: async (args) => { - // Test that all types are correctly mapped - const str: string = args.str - const int: number = args.int - const bool: boolean = args.bool - const user: string = args.user - const channel: string = args.channel - const role: string = args.role - const num: number = args.num - const mention: string = args.mention - const attach: string = args.attach - - expect(str).toBeDefined() - expect(int).toBeDefined() - expect(bool).toBeDefined() - expect(user).toBeDefined() - expect(channel).toBeDefined() - expect(role).toBeDefined() - expect(num).toBeDefined() - expect(mention).toBeDefined() - expect(attach).toBeDefined() + // Validate the args using arktype + const result = argsValidator(args) + + // Check if validation passed (result is the validated object, not wrapped) + if (result instanceof type.errors) { + expect.fail(`Validation failed: ${result.summary}`) + } else { + // Test that all types are correctly mapped + expect(typeof result.str).toBe('string') + expect(typeof result.int).toBe('number') + expect(Number.isInteger(result.int)).toBe(true) + expect(typeof result.bool).toBe('boolean') + expect(typeof result.user).toBe('string') + expect(typeof result.channel).toBe('string') + expect(typeof result.role).toBe('string') + expect(typeof result.num).toBe('number') + expect(typeof result.mention).toBe('string') + expect(typeof result.attach).toBe('string') + } }, } - expect(handlers).toBeDefined() + // Test with actual values + await handlers.types({ + str: 'hello', + int: 42, + bool: true, + user: '123456789', + channel: '987654321', + role: '555555555', + num: 3.14, + mention: '111111111', + attach: '999999999', + }) }) }) diff --git a/ts/src/discord/botevent/command_parser.ts b/ts/src/discord/botevent/command_parser.ts index b5a1009..f21700d 100644 --- a/ts/src/discord/botevent/command_parser.ts +++ b/ts/src/discord/botevent/command_parser.ts @@ -1,5 +1,9 @@ -import { ApplicationCommandOptionTypes, DiscordApplicationCommandOption, type CreateApplicationCommand, type DiscordInteractionDataOption } from '@discordeno/types' -import type { InteractionData } from '..' +import { ApplicationCommandOptionTypes } from '@discordeno/types' +import type {CreateApplicationCommand, + DiscordInteractionDataOption, + DiscordInteractionData, + DiscordApplicationCommandOption +}from '@discordeno/types' import type { SLASH_COMMANDS } from './slash_commands' // Map option types to their TypeScript types @@ -21,12 +25,12 @@ type GetOption = Options extends readonly DiscordApplicationComma // Extract the argument types from command options export type ExtractArgs = { [K in Options[number]['name']]: GetOption extends { type: infer T; required?: infer R } - ? T extends keyof OptionTypeMap - ? R extends true - ? OptionTypeMap[T] - : OptionTypeMap[T] | undefined - : never - : never + ? T extends keyof OptionTypeMap + ? R extends true + ? OptionTypeMap[T] + : OptionTypeMap[T] | undefined + : never + : never } // Handler function type that accepts typed arguments @@ -36,36 +40,36 @@ type HandlerFunction> = (args: Args) => Promise = Options extends readonly DiscordApplicationCommandOption[] - ? Options[number] extends infer O - ? O extends { name: Name; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } - ? O - : never - : never - : never +? Options[number] extends infer O +? O extends { name: Name; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } +? O +: never +: never +: never // Check if all options are subcommands or subcommand groups type HasOnlySubcommands = Options[number] extends { type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } ? true : false // Extract subcommand or subcommand group names from options type SubcommandNames = Options[number] extends { name: infer N; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } - ? N extends string - ? N - : never - : never +? N extends string +? N +: never +: never // Type to extract subcommand handlers (recursive for groups) export type SubcommandHandlers = { [K in SubcommandNames]: GetSubcommandOrGroup extends { type: infer T; options?: infer SubOpts } - ? T extends ApplicationCommandOptionTypes.SubCommandGroup - ? SubOpts extends readonly DiscordApplicationCommandOption[] - ? SubcommandHandlers - : never - : T extends ApplicationCommandOptionTypes.SubCommand - ? SubOpts extends readonly DiscordApplicationCommandOption[] - ? HandlerFunction> - : HandlerFunction> - : never - : never + ? T extends ApplicationCommandOptionTypes.SubCommandGroup + ? SubOpts extends readonly DiscordApplicationCommandOption[] + ? SubcommandHandlers + : never + : T extends ApplicationCommandOptionTypes.SubCommand + ? SubOpts extends readonly DiscordApplicationCommandOption[] + ? HandlerFunction> + : HandlerFunction> + : never + : never } // Get command by name from array @@ -74,12 +78,12 @@ type GetCommand = Commands[number] extend // Main type to extract command handlers from slash commands export type ExtractCommands = { [Name in T[number]['name']]: GetCommand extends { options?: infer Options } - ? Options extends readonly DiscordApplicationCommandOption[] - ? HasOnlySubcommands extends true - ? SubcommandHandlers - : HandlerFunction> - : HandlerFunction> - : HandlerFunction> + ? Options extends readonly DiscordApplicationCommandOption[] + ? HasOnlySubcommands extends true + ? SubcommandHandlers + : HandlerFunction> + : HandlerFunction> + : HandlerFunction> } // Type representing the possible output of ExtractCommands @@ -149,11 +153,11 @@ export function createCommandHandler - notFoundHandler: HandlerFunction<{path?: string}> -}) { - return async (data: InteractionData): Promise => { + commands: T, + handler: ExtractCommands + notFoundHandler: HandlerFunction<{path?: string}> + }) { + return async (data: DiscordInteractionData): Promise => { if (!data || !data.name) { await notFoundHandler({}) return diff --git a/ts/src/discord/botevent/slash_commands.ts b/ts/src/discord/botevent/slash_commands.ts index 96a3742..4315bbd 100644 --- a/ts/src/discord/botevent/slash_commands.ts +++ b/ts/src/discord/botevent/slash_commands.ts @@ -2,7 +2,7 @@ import { ApplicationCommandOptionTypes, ApplicationCommandTypes, type CreateAppl export const SLASH_COMMANDS = [ { - name: `guild`, + name: 'guild', description: 'guild commands', type: ApplicationCommandTypes.ChatInput, options: [ diff --git a/ts/src/discord/index.ts b/ts/src/discord/index.ts index 58c3d2d..779b5f2 100644 --- a/ts/src/discord/index.ts +++ b/ts/src/discord/index.ts @@ -1,4 +1,5 @@ import { Intents, type InteractionTypes } from '@discordeno/types' +import type { DiscordInteractionData} from '@discordeno/types' import type { Bot, CompleteDesiredProperties, DesiredPropertiesBehavior } from 'discordeno' export const intents = [ Intents.GuildModeration, @@ -56,11 +57,8 @@ export interface InteractionRef { acknowledged?: boolean } -// Type for the interaction data payload -export type InteractionData = Parameters>[0]['data'] - // Type for the complete interaction handling payload export interface InteractionCreatePayload { ref: InteractionRef - data: InteractionData + data: DiscordInteractionData } diff --git a/ts/src/discord/mux/index.ts b/ts/src/discord/mux/index.ts index ba68d60..cebe75d 100644 --- a/ts/src/discord/mux/index.ts +++ b/ts/src/discord/mux/index.ts @@ -1,5 +1,4 @@ import { c } from '#/di' export class EventMux { - constructor() {} } diff --git a/ts/src/lib/util/tabwriter.ts b/ts/src/lib/util/tabwriter.ts index 86b97db..b6c9c0d 100644 --- a/ts/src/lib/util/tabwriter.ts +++ b/ts/src/lib/util/tabwriter.ts @@ -6,10 +6,10 @@ export class TabWriter { } add(row: string[]) { - if (this.columns.length == 0) { + if (this.columns.length === 0) { this.columns = new Array(row.length).fill(0).map(() => []) } - if (row.length != this.columns.length) { + if (row.length !== this.columns.length) { throw new Error(`Row length ${row.length} does not match columns length ${this.columns.length}`) } for (let i = 0; i < row.length; i++) { @@ -19,7 +19,7 @@ export class TabWriter { build() { let out = '' - if (this.columns.length == 0) { + if (this.columns.length === 0) { return '' } const columnWidths = this.columns.map((col) => col.reduce((a, b) => Math.max(a, b.length + this.spacing), 0)) diff --git a/ts/src/lib/wynn/wapi.ts b/ts/src/lib/wynn/wapi.ts index 8b2c8fb..4c53c2f 100644 --- a/ts/src/lib/wynn/wapi.ts +++ b/ts/src/lib/wynn/wapi.ts @@ -42,7 +42,7 @@ export class WApi { : // When a stale state has a determined value to expire, we can use it. // Or if the cached value cannot enter in stale state. (value.state === 'stale' && value.ttl) || (value.state === 'cached' && !canStale(value)) - ? value.createdAt + value.ttl! + ? value.createdAt + (value.ttl || 0) : // otherwise, we can't determine when it should expire, so we keep // it indefinitely. undefined diff --git a/ts/src/payload_converter/adapter.ts b/ts/src/payload_converter/adapter.ts index 5324c85..ae97af6 100644 --- a/ts/src/payload_converter/adapter.ts +++ b/ts/src/payload_converter/adapter.ts @@ -12,7 +12,7 @@ export class SuperJsonPayloadConverter implements PayloadConverterWithEncoding { public toPayload(value: unknown): Payload | undefined { if (value === undefined) return undefined - let ejson + let ejson: any try { ejson = superjson.stringify(value) } catch (e) { diff --git a/ts/src/workflows/discord.ts b/ts/src/workflows/discord.ts index c785f71..0d85c54 100644 --- a/ts/src/workflows/discord.ts +++ b/ts/src/workflows/discord.ts @@ -26,13 +26,13 @@ const workflowHandleApplicationCommand = async (payload: InteractionCreatePayloa }) } if (!data || !data.name) { - await notFoundHandler(`Invalid command data`) + await notFoundHandler('Invalid command data') return } const commandHandler = createCommandHandler({ commands: SLASH_COMMANDS, notFoundHandler: async () => { - await notFoundHandler(`command not found`) + await notFoundHandler('command not found') }, handler: { player: {