noot
This commit is contained in:
parent
24c7c2b9b1
commit
a8295b09d6
Binary file not shown.
@ -2,14 +2,24 @@
|
||||
"name": "backend",
|
||||
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
|
||||
"scripts": {
|
||||
"barrels": "barrelsby -c barrelsby.json --delete"
|
||||
"barrels": "barrelsby -c barrelsby.json --delete",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:typecheck": "vitest typecheck",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/object-hash": "^3",
|
||||
"@vitest/runner": "^3.2.3",
|
||||
"@vitest/ui": "^3.2.3",
|
||||
"barrelsby": "^2.8.1",
|
||||
"happy-dom": "^18.0.1",
|
||||
"knip": "^5.45.0",
|
||||
"rollup": "^4.34.8",
|
||||
"typescript": "5.7.3"
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordeno/types": "^21.0.0",
|
||||
@ -21,7 +31,6 @@
|
||||
"@temporalio/workflow": "^1.11.7",
|
||||
"@ts-rest/core": "https://gitpkg.vercel.app/aidant/ts-rest/libs/ts-rest/core?feat-standard-schema",
|
||||
"@ts-rest/fastify": "https://gitpkg.vercel.app/aidant/ts-rest/libs/ts-rest/fastify?feat-standard-schema",
|
||||
"@types/node": "^22.13.4",
|
||||
"any-date-parser": "^2.0.3",
|
||||
"arktype": "2.1.1",
|
||||
"axios": "^1.7.9",
|
||||
|
||||
428
ts/src/discord/botevent/command_parser.spec.ts
Normal file
428
ts/src/discord/botevent/command_parser.spec.ts
Normal file
@ -0,0 +1,428 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from '@discordeno/types'
|
||||
import { createCommandHandler, ExtractCommands } from './command_parser'
|
||||
import type { InteractionData } from '..'
|
||||
|
||||
// Test command definitions
|
||||
const TEST_COMMANDS = [
|
||||
{
|
||||
name: 'simple',
|
||||
description: 'A simple command',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: 'message',
|
||||
description: 'A message to echo',
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'count',
|
||||
description: 'Number of times',
|
||||
type: ApplicationCommandOptionTypes.Integer,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'complex',
|
||||
description: 'A command with subcommands',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: 'list',
|
||||
description: 'List items',
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
options: [
|
||||
{
|
||||
name: 'filter',
|
||||
description: 'Filter string',
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'create',
|
||||
description: 'Create item',
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
options: [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'Item name',
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'enabled',
|
||||
description: 'Is enabled',
|
||||
type: ApplicationCommandOptionTypes.Boolean,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies CreateApplicationCommand[]
|
||||
|
||||
describe('createCommandHandler', () => {
|
||||
it('should handle a simple command with required string argument', async () => {
|
||||
const simpleHandler = vi.fn()
|
||||
const notFoundHandler = vi.fn()
|
||||
|
||||
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||||
handler: {
|
||||
simple: simpleHandler,
|
||||
complex: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
notFoundHandler,
|
||||
})
|
||||
|
||||
const interactionData: InteractionData = {
|
||||
name: 'simple',
|
||||
options: [
|
||||
{ name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello world' },
|
||||
],
|
||||
}
|
||||
|
||||
await handler(interactionData)
|
||||
|
||||
expect(simpleHandler).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()
|
||||
|
||||
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||||
handler: {
|
||||
simple: simpleHandler,
|
||||
complex: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
notFoundHandler,
|
||||
})
|
||||
|
||||
const interactionData: InteractionData = {
|
||||
name: 'simple',
|
||||
options: [
|
||||
{ name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello' },
|
||||
{ name: 'count', type: ApplicationCommandOptionTypes.Integer, value: 5 },
|
||||
],
|
||||
}
|
||||
|
||||
await handler(interactionData)
|
||||
|
||||
expect(simpleHandler).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<typeof TEST_COMMANDS>({
|
||||
handler: {
|
||||
simple: vi.fn(),
|
||||
complex: {
|
||||
list: listHandler,
|
||||
create: createHandler,
|
||||
},
|
||||
},
|
||||
notFoundHandler,
|
||||
})
|
||||
|
||||
const interactionData: InteractionData = {
|
||||
name: 'complex',
|
||||
options: [
|
||||
{
|
||||
name: 'create',
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
options: [
|
||||
{ name: 'name', type: ApplicationCommandOptionTypes.String, value: 'Test Item' },
|
||||
{ name: 'enabled', type: ApplicationCommandOptionTypes.Boolean, value: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await handler(interactionData)
|
||||
|
||||
expect(createHandler).toHaveBeenCalledWith({ name: 'Test Item', enabled: true })
|
||||
expect(listHandler).not.toHaveBeenCalled()
|
||||
expect(notFoundHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call notFoundHandler for unknown commands', async () => {
|
||||
const notFoundHandler = vi.fn()
|
||||
|
||||
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||||
handler: {
|
||||
simple: vi.fn(),
|
||||
complex: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
notFoundHandler,
|
||||
})
|
||||
|
||||
const interactionData: InteractionData = {
|
||||
name: 'unknown',
|
||||
options: [],
|
||||
}
|
||||
|
||||
await handler(interactionData)
|
||||
|
||||
expect(notFoundHandler).toHaveBeenCalledWith({})
|
||||
})
|
||||
|
||||
it('should call notFoundHandler for unknown subcommands', async () => {
|
||||
const notFoundHandler = vi.fn()
|
||||
|
||||
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||||
handler: {
|
||||
simple: vi.fn(),
|
||||
complex: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
notFoundHandler,
|
||||
})
|
||||
|
||||
const interactionData: InteractionData = {
|
||||
name: 'complex',
|
||||
options: [
|
||||
{
|
||||
name: 'unknown',
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await handler(interactionData)
|
||||
|
||||
expect(notFoundHandler).toHaveBeenCalledWith({})
|
||||
})
|
||||
|
||||
it('should handle missing interaction data', async () => {
|
||||
const notFoundHandler = vi.fn()
|
||||
|
||||
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||||
handler: {
|
||||
simple: vi.fn(),
|
||||
complex: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
notFoundHandler,
|
||||
})
|
||||
|
||||
await handler(null as any)
|
||||
expect(notFoundHandler).toHaveBeenCalledWith({})
|
||||
|
||||
await handler({} as any)
|
||||
expect(notFoundHandler).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should handle commands without options', async () => {
|
||||
const simpleHandler = vi.fn()
|
||||
|
||||
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||||
handler: {
|
||||
simple: simpleHandler,
|
||||
complex: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
notFoundHandler: vi.fn(),
|
||||
})
|
||||
|
||||
const interactionData: InteractionData = {
|
||||
name: 'simple',
|
||||
// No options provided
|
||||
}
|
||||
|
||||
await handler(interactionData)
|
||||
|
||||
expect(simpleHandler).toHaveBeenCalledWith({})
|
||||
})
|
||||
|
||||
it('should ignore subcommand options when parsing top-level args', async () => {
|
||||
const simpleHandler = vi.fn()
|
||||
|
||||
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||||
handler: {
|
||||
simple: simpleHandler,
|
||||
complex: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
notFoundHandler: vi.fn(),
|
||||
})
|
||||
|
||||
const interactionData: InteractionData = {
|
||||
name: 'simple',
|
||||
options: [
|
||||
{ name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello' },
|
||||
// This should be ignored in parseOptions
|
||||
{ name: 'subcommand', type: ApplicationCommandOptionTypes.SubCommand, options: [] },
|
||||
],
|
||||
}
|
||||
|
||||
await handler(interactionData)
|
||||
|
||||
expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('ExtractCommands type utility', () => {
|
||||
it('should correctly extract command types', () => {
|
||||
// Type tests - these compile-time checks ensure the types work correctly
|
||||
type TestHandlers = ExtractCommands<typeof TEST_COMMANDS>
|
||||
|
||||
// Test that the type structure is correct
|
||||
const handlers: TestHandlers = {
|
||||
simple: async (args) => {
|
||||
// TypeScript should know that args has message as required string and count as optional number
|
||||
const message: string = args.message
|
||||
const count: number | undefined = args.count
|
||||
expect(message).toBeDefined()
|
||||
expect(count).toBeDefined()
|
||||
},
|
||||
complex: {
|
||||
list: async (args) => {
|
||||
// TypeScript should know that args has filter as optional string
|
||||
const filter: string | undefined = args.filter
|
||||
expect(filter).toBeDefined()
|
||||
},
|
||||
create: async (args) => {
|
||||
// TypeScript should know that args has name as required string and enabled as optional boolean
|
||||
const name: string = args.name
|
||||
const enabled: boolean | undefined = args.enabled
|
||||
expect(name).toBeDefined()
|
||||
expect(enabled).toBeDefined()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// This is just to satisfy TypeScript - the real test is that the above compiles
|
||||
expect(handlers).toBeDefined()
|
||||
})
|
||||
|
||||
it('should infer types correctly without manual typing', () => {
|
||||
// This test verifies that args are inferred correctly without manual type annotations
|
||||
const handler = createCommandHandler<typeof TEST_COMMANDS>({
|
||||
notFoundHandler: async () => {},
|
||||
handler: {
|
||||
simple: async (args) => {
|
||||
// args should be automatically typed
|
||||
// This would error if args.message wasn't a string
|
||||
const upper = args.message.toUpperCase()
|
||||
// This would error if args.count wasn't number | undefined
|
||||
const doubled = args.count ? args.count * 2 : 0
|
||||
expect(upper).toBeDefined()
|
||||
expect(doubled).toBeDefined()
|
||||
},
|
||||
complex: {
|
||||
list: async (args) => {
|
||||
// This would error if args.filter wasn't string | undefined
|
||||
const filtered = args.filter?.toLowerCase()
|
||||
expect(filtered !== undefined).toBeDefined()
|
||||
},
|
||||
create: async (args) => {
|
||||
// This would error if types weren't correct
|
||||
const nameLength = args.name.length
|
||||
const isEnabled = args.enabled === true
|
||||
expect(nameLength).toBeDefined()
|
||||
expect(isEnabled).toBeDefined()
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(handler).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle commands with no options', () => {
|
||||
const EMPTY_COMMANDS = [
|
||||
{
|
||||
name: 'ping',
|
||||
description: 'Ping command',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
},
|
||||
] as const satisfies CreateApplicationCommand[]
|
||||
|
||||
type EmptyHandlers = ExtractCommands<typeof EMPTY_COMMANDS>
|
||||
|
||||
const handlers: EmptyHandlers = {
|
||||
ping: async (args) => {
|
||||
// args should be an empty object
|
||||
expect(args).toEqual({})
|
||||
},
|
||||
}
|
||||
|
||||
expect(handlers).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle different option types', () => {
|
||||
const TYPE_TEST_COMMANDS = [
|
||||
{
|
||||
name: 'types',
|
||||
description: 'Test different types',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
options: [
|
||||
{ name: 'str', description: 'String param', type: ApplicationCommandOptionTypes.String, required: true },
|
||||
{ name: 'int', description: 'Integer param', type: ApplicationCommandOptionTypes.Integer, required: true },
|
||||
{ name: 'bool', description: 'Boolean param', type: ApplicationCommandOptionTypes.Boolean, required: true },
|
||||
{ name: 'user', description: 'User param', type: ApplicationCommandOptionTypes.User, required: true },
|
||||
{ name: 'channel', description: 'Channel param', type: ApplicationCommandOptionTypes.Channel, required: true },
|
||||
{ name: 'role', description: 'Role param', type: ApplicationCommandOptionTypes.Role, required: true },
|
||||
{ name: 'num', description: 'Number param', type: ApplicationCommandOptionTypes.Number, required: true },
|
||||
{ name: 'mention', description: 'Mentionable param', type: ApplicationCommandOptionTypes.Mentionable, required: true },
|
||||
{ name: 'attach', description: 'Attachment param', type: ApplicationCommandOptionTypes.Attachment, required: true },
|
||||
],
|
||||
},
|
||||
] as const satisfies CreateApplicationCommand[]
|
||||
|
||||
type TypeHandlers = ExtractCommands<typeof TYPE_TEST_COMMANDS>
|
||||
|
||||
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()
|
||||
},
|
||||
}
|
||||
|
||||
expect(handlers).toBeDefined()
|
||||
})
|
||||
})
|
||||
@ -15,15 +15,18 @@ type OptionTypeMap = {
|
||||
[ApplicationCommandOptionTypes.Attachment]: string; // attachment ID
|
||||
};
|
||||
|
||||
// Helper type to get option by name
|
||||
type GetOption<Options, Name> = Options extends readonly any[]
|
||||
? Options[number] extends infer O
|
||||
? O extends { name: Name }
|
||||
? O
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
// Extract the argument types from command options
|
||||
type ExtractArgs<Options extends readonly any[]> = {
|
||||
[K in Options[number] as K extends { name: infer N; type: infer T }
|
||||
? T extends keyof OptionTypeMap
|
||||
? N extends string
|
||||
? N
|
||||
: never
|
||||
: never
|
||||
: never]: Options[number] extends { name: K; type: infer T; required?: infer R }
|
||||
export type ExtractArgs<Options extends readonly any[]> = {
|
||||
[K in Options[number]['name']]: GetOption<Options, K> extends { type: infer T; required?: infer R }
|
||||
? T extends keyof OptionTypeMap
|
||||
? R extends true
|
||||
? OptionTypeMap[T]
|
||||
@ -35,31 +38,54 @@ type ExtractArgs<Options extends readonly any[]> = {
|
||||
// Handler function type that accepts typed arguments
|
||||
type HandlerFunction<Args = {}> = (args: Args) => Promise<void> | void;
|
||||
|
||||
// Helper types to extract command structure with proper argument types
|
||||
type ExtractSubcommands<T> = T extends { options: infer O }
|
||||
? O extends readonly any[]
|
||||
? {
|
||||
[K in O[number] as K extends { name: infer N; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup }
|
||||
? N extends string
|
||||
? N
|
||||
// Get subcommand by name
|
||||
type GetSubcommand<Options, Name> = Options extends readonly any[]
|
||||
? Options[number] extends infer O
|
||||
? O extends { name: Name; type: ApplicationCommandOptionTypes.SubCommand }
|
||||
? O
|
||||
: never
|
||||
: never]: O[number] extends { name: K; options?: infer SubO }
|
||||
? SubO extends readonly any[]
|
||||
? HandlerFunction<ExtractArgs<SubO>>
|
||||
: HandlerFunction<{}>
|
||||
: HandlerFunction<{}>
|
||||
}
|
||||
: never
|
||||
: never;
|
||||
|
||||
type ExtractCommands<T extends readonly CreateApplicationCommand[]> = {
|
||||
[K in T[number] as K['name']]: ExtractSubcommands<K> extends never
|
||||
? T[number] extends { name: K; options?: infer O }
|
||||
? O extends readonly any[]
|
||||
? HandlerFunction<ExtractArgs<O>>
|
||||
// Check if all options are subcommands
|
||||
type HasOnlySubcommands<Options extends readonly any[]> =
|
||||
Options[number] extends { type: ApplicationCommandOptionTypes.SubCommand }
|
||||
? true
|
||||
: false;
|
||||
|
||||
// Extract subcommand names from options
|
||||
type SubcommandNames<Options extends readonly any[]> =
|
||||
Options[number] extends { name: infer N; type: ApplicationCommandOptionTypes.SubCommand }
|
||||
? N extends string
|
||||
? N
|
||||
: never
|
||||
: never;
|
||||
|
||||
// Type to extract subcommand handlers
|
||||
export type SubcommandHandlers<Options extends readonly any[]> = {
|
||||
[K in SubcommandNames<Options>]: GetSubcommand<Options, K> extends { options?: infer SubOpts }
|
||||
? SubOpts extends readonly any[]
|
||||
? HandlerFunction<ExtractArgs<SubOpts>>
|
||||
: HandlerFunction<{}>
|
||||
: HandlerFunction<{}>
|
||||
};
|
||||
|
||||
// Get command by name from array
|
||||
type GetCommand<Commands extends readonly any[], Name> = Commands[number] extends infer C
|
||||
? C extends { name: Name }
|
||||
? C
|
||||
: never
|
||||
: never;
|
||||
|
||||
// Main type to extract command handlers from slash commands
|
||||
export type ExtractCommands<T extends readonly any[]> = {
|
||||
[Name in T[number]['name']]: GetCommand<T, Name> extends { options?: infer Options }
|
||||
? Options extends readonly any[]
|
||||
? HasOnlySubcommands<Options> extends true
|
||||
? SubcommandHandlers<Options>
|
||||
: HandlerFunction<ExtractArgs<Options>>
|
||||
: HandlerFunction<{}>
|
||||
: HandlerFunction<{}>
|
||||
: ExtractSubcommands<K>
|
||||
};
|
||||
|
||||
// The actual command handler type based on SLASH_COMMANDS
|
||||
@ -128,62 +154,8 @@ export function createCommandHandler<T extends readonly CreateApplicationCommand
|
||||
|
||||
// Parse arguments from subcommand options
|
||||
const args = parseOptions(subcommand.options);
|
||||
await subHandler(args);
|
||||
await (subHandler as HandlerFunction<any>)(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to validate that all commands are implemented
|
||||
export function validateCommandImplementation(handlers: CommandHandlers): void {
|
||||
for (const command of SLASH_COMMANDS) {
|
||||
const commandName = command.name;
|
||||
if (!(commandName in handlers)) {
|
||||
throw new Error(`Command "${commandName}" is not implemented`);
|
||||
}
|
||||
|
||||
// Check subcommands if they exist
|
||||
if (command.options) {
|
||||
const subcommands = command.options.filter(
|
||||
opt => opt.type === ApplicationCommandOptionTypes.SubCommand
|
||||
);
|
||||
|
||||
if (subcommands.length > 0) {
|
||||
const handlerObj = handlers[commandName as keyof CommandHandlers];
|
||||
if (typeof handlerObj === 'function') {
|
||||
throw new Error(`Command "${commandName}" has subcommands but is implemented as a function`);
|
||||
}
|
||||
|
||||
for (const subcommand of subcommands) {
|
||||
if (!(subcommand.name in handlerObj)) {
|
||||
throw new Error(`Subcommand "${commandName}.${subcommand.name}" is not implemented`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create command handlers with type safety
|
||||
export function createCommandHandlers<T extends CommandHandlers>(handlers: T): T {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
// Example usage showing the type-safe implementation
|
||||
export const exampleHandlers = createCommandHandlers({
|
||||
guild: {
|
||||
leaderboard: async (args: {}) => {
|
||||
console.log("Guild leaderboard command");
|
||||
},
|
||||
info: async (args: {}) => {
|
||||
console.log("Guild info command");
|
||||
},
|
||||
online: async (args: {}) => {
|
||||
console.log("Guild online command");
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
set_wynn_guild: async (args: {}) => {
|
||||
console.log("Admin set_wynn_guild command");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -37,5 +37,24 @@ export const SLASH_COMMANDS = [
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
},
|
||||
],
|
||||
},{
|
||||
name: "player",
|
||||
description: "player commands",
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "lookup",
|
||||
description: "view player information",
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
options: [
|
||||
{
|
||||
name: "player",
|
||||
description: "player name",
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
] as const satisfies CreateApplicationCommand[]
|
||||
|
||||
@ -2,6 +2,7 @@ import { proxyActivities, startChild, workflowInfo } from '@temporalio/workflow'
|
||||
import type * as activities from '#/activities';
|
||||
import { InteractionTypes } from '@discordeno/types';
|
||||
import { handleCommandGuildInfo, handleCommandGuildOnline, handleCommandGuildLeaderboard } from './guild_messages';
|
||||
import { handleCommandPlayerLookup } from './player_messages';
|
||||
import { createCommandHandler } from '#/discord/botevent/command_parser';
|
||||
import { SLASH_COMMANDS } from '#/discord/botevent/slash_commands';
|
||||
import { InteractionCreatePayload} from '#/discord';
|
||||
@ -34,9 +35,19 @@ const workflowHandleApplicationCommand = async (
|
||||
notFoundHandler: async () => {
|
||||
await notFoundHandler(`command not found`);
|
||||
},
|
||||
handler:{
|
||||
handler: {
|
||||
player: {
|
||||
lookup: async (args) => {
|
||||
const { workflowId } = workflowInfo();
|
||||
const handle = await startChild(handleCommandPlayerLookup, {
|
||||
args: [{ ref, args }],
|
||||
workflowId: `${workflowId}-player-lookup`,
|
||||
});
|
||||
await handle.result();
|
||||
},
|
||||
},
|
||||
guild: {
|
||||
info: async (args: {}) => {
|
||||
info: async (args) => {
|
||||
const { workflowId } = workflowInfo();
|
||||
const handle = await startChild(handleCommandGuildInfo, {
|
||||
args: [{ ref }],
|
||||
@ -44,7 +55,7 @@ const workflowHandleApplicationCommand = async (
|
||||
});
|
||||
await handle.result();
|
||||
},
|
||||
online: async (args: {}) => {
|
||||
online: async (args) => {
|
||||
const { workflowId } = workflowInfo();
|
||||
const handle = await startChild(handleCommandGuildOnline, {
|
||||
args: [{ ref }],
|
||||
@ -52,7 +63,7 @@ const workflowHandleApplicationCommand = async (
|
||||
});
|
||||
await handle.result();
|
||||
},
|
||||
leaderboard: async (args: {}) => {
|
||||
leaderboard: async (args) => {
|
||||
const { workflowId } = workflowInfo();
|
||||
const handle = await startChild(handleCommandGuildLeaderboard, {
|
||||
args: [{ ref }],
|
||||
@ -62,7 +73,7 @@ const workflowHandleApplicationCommand = async (
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
set_wynn_guild: async (args: {}) => {
|
||||
set_wynn_guild: async (args) => {
|
||||
await reply_to_interaction({
|
||||
ref,
|
||||
type: 4,
|
||||
|
||||
@ -6,4 +6,5 @@ export * from "./discord";
|
||||
export * from "./guild_messages";
|
||||
export * from "./guilds";
|
||||
export * from "./items";
|
||||
export * from "./player_messages";
|
||||
export * from "./players";
|
||||
|
||||
43
ts/src/workflows/player_messages.ts
Normal file
43
ts/src/workflows/player_messages.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { proxyActivities } from "@temporalio/workflow";
|
||||
import type * as activities from "#/activities";
|
||||
import { InteractionRef } from "#/discord";
|
||||
|
||||
const {
|
||||
reply_to_interaction
|
||||
} = proxyActivities<typeof activities>({
|
||||
startToCloseTimeout: '30 seconds',
|
||||
});
|
||||
|
||||
interface CommandPayload {
|
||||
ref: InteractionRef;
|
||||
args: {
|
||||
player: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleCommandPlayerLookup(payload: CommandPayload): Promise<void> {
|
||||
const { ref, args } = payload;
|
||||
const playerName = args.player;
|
||||
|
||||
try {
|
||||
// For now, we'll send a simple response
|
||||
// In the future, we can fetch player stats from Wynncraft API
|
||||
await reply_to_interaction({
|
||||
ref,
|
||||
type: 4,
|
||||
options: {
|
||||
content: `Looking up player: **${playerName}**\n\n*Player lookup functionality coming soon!*`,
|
||||
isPrivate: false,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
await reply_to_interaction({
|
||||
ref,
|
||||
type: 4,
|
||||
options: {
|
||||
content: `Error looking up player: ${playerName}`,
|
||||
isPrivate: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
18
ts/vitest.config.ts
Normal file
18
ts/vitest.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
typecheck: {
|
||||
enabled: true,
|
||||
tsconfig: './tsconfig.json',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'#': new URL('./src', import.meta.url).pathname,
|
||||
},
|
||||
},
|
||||
})
|
||||
1065
ts/yarn.lock
1065
ts/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user