diff --git a/migrations/yarn.lock b/migrations/yarn.lock deleted file mode 100644 index f3438ff..0000000 --- a/migrations/yarn.lock +++ /dev/null @@ -1,160 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-bound@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" - integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== - dependencies: - call-bind-apply-helpers "^1.0.1" - get-intrinsic "^1.2.6" - -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -es-define-property@^1.0.0, es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -get-intrinsic@^1.2.4, get-intrinsic@^1.2.6: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -gopd@^1.0.1, gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -json-stable-stringify@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz#addb683c2b78014d0b78d704c2fcbdf0695a60e2" - integrity sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA== - dependencies: - call-bind "^1.0.8" - call-bound "^1.0.3" - isarray "^2.0.5" - jsonify "^0.0.1" - object-keys "^1.1.1" - -jsonify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" - integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" diff --git a/ts/.yarn/install-state.gz b/ts/.yarn/install-state.gz index 68e99ca..68ef70b 100644 Binary files a/ts/.yarn/install-state.gz and b/ts/.yarn/install-state.gz differ diff --git a/ts/package.json b/ts/package.json index 8757b8b..49463b8 100644 --- a/ts/package.json +++ b/ts/package.json @@ -12,6 +12,7 @@ "typescript": "5.7.3" }, "dependencies": { + "@discordeno/types": "^21.0.0", "@needle-di/core": "^0.10.1", "@temporalio/activity": "^1.11.7", "@temporalio/client": "^1.11.7", diff --git a/ts/src/activities/discord.ts b/ts/src/activities/discord.ts index 3459b94..b1ad354 100644 --- a/ts/src/activities/discord.ts +++ b/ts/src/activities/discord.ts @@ -1,14 +1,7 @@ -import { Bot } from "#/bot"; +import { Bot } from "#/discord/bot"; import {c} from "#/di" import { InteractionResponseTypes, InteractionCallbackOptions, InteractionCallbackData, InteractionTypes, MessageFlags } from "discordeno"; - - -export interface InteractionRef { - id: bigint - token: string - type: InteractionTypes - acknowledged?: boolean -} +import { InteractionRef } from "#/discord"; // from https://github.com/discordeno/discordeno/blob/21.0.0/packages/bot/src/transformers/interaction.ts#L33 export const reply_to_interaction = async (props: { diff --git a/ts/src/activities/guild-messages.ts b/ts/src/activities/guild_messages.ts similarity index 100% rename from ts/src/activities/guild-messages.ts rename to ts/src/activities/guild_messages.ts diff --git a/ts/src/activities/index.ts b/ts/src/activities/index.ts index b6c8f48..d39f5f7 100644 --- a/ts/src/activities/index.ts +++ b/ts/src/activities/index.ts @@ -4,7 +4,7 @@ export * from "./database"; export * from "./discord"; -export * from "./guild-messages"; export * from "./guild"; +export * from "./guild_messages"; export * from "./leaderboards"; export * from "./players"; diff --git a/ts/src/cmd/bot.ts b/ts/src/cmd/bot.ts index ae43e37..85190fc 100644 --- a/ts/src/cmd/bot.ts +++ b/ts/src/cmd/bot.ts @@ -4,9 +4,9 @@ import { Command } from 'clipanion'; // di import "#/services/pg" import { DISCORD_GUILD_ID } from '#/constants'; -import { Bot } from '#/bot'; -import { events } from '#/bot/botevent/handler'; -import { SLASH_COMMANDS } from '#/bot/botevent/slash_commands'; +import { Bot } from '#/discord/bot'; +import { events } from '#/discord/botevent/handler'; +import { SLASH_COMMANDS } from '#/discord/botevent/slash_commands'; import { c } from '#/di'; import { config } from '#/config'; diff --git a/ts/src/cmd/worker.ts b/ts/src/cmd/worker.ts index be0f063..dbb4ddd 100644 --- a/ts/src/cmd/worker.ts +++ b/ts/src/cmd/worker.ts @@ -117,7 +117,7 @@ export class WorkerCommand extends Command { namespace: config.TEMPORAL_NAMESPACE, workflowsPath: require.resolve('../workflows'), dataConverter: { - payloadConverterPath: require.resolve('../payload-converter'), + payloadConverterPath: require.resolve('../payload_converter'), }, bundlerOptions: { webpackConfigHook: (config)=>{ diff --git a/ts/src/discord/bot.ts b/ts/src/discord/bot.ts new file mode 100644 index 0000000..130f230 --- /dev/null +++ b/ts/src/discord/bot.ts @@ -0,0 +1,24 @@ +import { config } from "#/config"; +import { c } from "#/di"; +import { InjectionToken } from "@needle-di/core"; +import { createBot, } from "discordeno"; +import { BotType, createBotParameters } from "./index"; + +const createBotWithToken = (token: string) => { + return createBot({ + ...createBotParameters, + token, + }) +} +export const Bot = new InjectionToken("DISCORD_BOT") +c.bind({ + provide: Bot, + useFactory: () => { + let token = config.DISCORD_TOKEN + if(!token) { + throw new Error('no discord token found. bot cant start'); + } + const bot = createBotWithToken(token) + return bot + }, +}) diff --git a/ts/src/discord/botevent/command_parser.ts b/ts/src/discord/botevent/command_parser.ts new file mode 100644 index 0000000..38cc16a --- /dev/null +++ b/ts/src/discord/botevent/command_parser.ts @@ -0,0 +1,189 @@ +import { ApplicationCommandOptionTypes, CreateApplicationCommand, DiscordInteractionDataOption} from "@discordeno/types"; +import { SLASH_COMMANDS } from "./slash_commands"; +import { InteractionData } from ".."; + +// Map option types to their TypeScript types +type OptionTypeMap = { + [ApplicationCommandOptionTypes.String]: string; + [ApplicationCommandOptionTypes.Integer]: number; + [ApplicationCommandOptionTypes.Boolean]: boolean; + [ApplicationCommandOptionTypes.User]: string; // user ID + [ApplicationCommandOptionTypes.Channel]: string; // channel ID + [ApplicationCommandOptionTypes.Role]: string; // role ID + [ApplicationCommandOptionTypes.Number]: number; + [ApplicationCommandOptionTypes.Mentionable]: string; // ID + [ApplicationCommandOptionTypes.Attachment]: string; // attachment ID +}; + +// Extract the argument types from command options +type ExtractArgs = { + [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 } + ? T extends keyof OptionTypeMap + ? R extends true + ? OptionTypeMap[T] + : OptionTypeMap[T] | undefined + : never + : never; +}; + +// Handler function type that accepts typed arguments +type HandlerFunction = (args: Args) => Promise | void; + +// Helper types to extract command structure with proper argument types +type ExtractSubcommands = 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 + : never + : never]: O[number] extends { name: K; options?: infer SubO } + ? SubO extends readonly any[] + ? HandlerFunction> + : HandlerFunction<{}> + : HandlerFunction<{}> + } + : never + : never; + +type ExtractCommands = { + [K in T[number] as K['name']]: ExtractSubcommands extends never + ? T[number] extends { name: K; options?: infer O } + ? O extends readonly any[] + ? HandlerFunction> + : HandlerFunction<{}> + : HandlerFunction<{}> + : ExtractSubcommands +}; + +// The actual command handler type based on SLASH_COMMANDS +export type CommandHandlers = ExtractCommands; + +// Helper function to parse option values from interaction data +function parseOptions(options?: DiscordInteractionDataOption[]): Record { + if (!options) return {}; + + const args: Record = {}; + + for (const option of options) { + if (option.type === ApplicationCommandOptionTypes.SubCommand || + option.type === ApplicationCommandOptionTypes.SubCommandGroup) { + continue; + } + + args[option.name] = option.value; + } + + return args; +} + +// Helper function to create command handlers with type safety +export function createCommandHandler( + {handler, notFoundHandler}:{ + handler: ExtractCommands + notFoundHandler: HandlerFunction<{}> +}) { + return async (data: InteractionData): Promise => { + if (!data || !data.name) { + await notFoundHandler({}); + return; + } + + const commandName = data.name as keyof typeof handler; + const commandHandler = handler[commandName]; + + if (!commandHandler) { + await notFoundHandler({}); + return; + } + + // Check if it's a direct command or has subcommands + if (typeof commandHandler === 'function') { + // Parse arguments from top-level options + const args = parseOptions(data.options); + await commandHandler(args); + } else { + // Handle subcommands + const subcommand = data.options?.find( + opt => opt.type === ApplicationCommandOptionTypes.SubCommand || + opt.type === ApplicationCommandOptionTypes.SubCommandGroup + ); + + if (!subcommand) { + await notFoundHandler({}); + return; + } + + const subHandler = commandHandler[subcommand.name as keyof typeof commandHandler]; + if (!subHandler || typeof subHandler !== 'function') { + await notFoundHandler({}); + return; + } + + // Parse arguments from subcommand options + const args = parseOptions(subcommand.options); + await subHandler(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(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"); + }, + }, +}); diff --git a/ts/src/bot/botevent/handler.ts b/ts/src/discord/botevent/handler.ts similarity index 92% rename from ts/src/bot/botevent/handler.ts rename to ts/src/discord/botevent/handler.ts index d5f623b..200cf42 100644 --- a/ts/src/bot/botevent/handler.ts +++ b/ts/src/discord/botevent/handler.ts @@ -1,10 +1,11 @@ -import { Bot, BotType } from "#/bot" +import { Bot } from "#/discord/bot" import { ActivityTypes, InteractionTypes } from "discordeno" import { c } from "#/di" import { Client } from "@temporalio/client" import { workflowHandleInteractionCreate } from "#/workflows" +import { BotType } from "#/discord" -export const events = () => {return { +export const events = () => {return { interactionCreate: async (interaction) => { if(interaction.acknowledged) { return @@ -12,9 +13,9 @@ export const events = () => {return { if(interaction.type !== InteractionTypes.ApplicationCommand) { return } - + const temporalClient = await c.getAsync(Client); - + // Start the workflow to handle the interaction const handle = await temporalClient.workflow.start(workflowHandleInteractionCreate, { args: [{ @@ -29,10 +30,10 @@ export const events = () => {return { workflowId: `discord-interaction-${interaction.id}`, taskQueue: 'wynn-worker-ts', }); - + // Wait for the workflow to complete await handle.result(); - + return }, ready: async ({shardId}) => { diff --git a/ts/src/bot/botevent/slash_commands.ts b/ts/src/discord/botevent/slash_commands.ts similarity index 83% rename from ts/src/bot/botevent/slash_commands.ts rename to ts/src/discord/botevent/slash_commands.ts index c023ee6..ec3c9d7 100644 --- a/ts/src/bot/botevent/slash_commands.ts +++ b/ts/src/discord/botevent/slash_commands.ts @@ -1,9 +1,6 @@ -import { ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from "discordeno" +import { ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from "@discordeno/types" -const createCommands = (commands: T): T => { - return commands -} -export const SLASH_COMMANDS = createCommands([ +export const SLASH_COMMANDS = [ { name: `guild`, description: "guild commands", @@ -41,4 +38,4 @@ export const SLASH_COMMANDS = createCommands([ }, ], } -]) +] as const satisfies CreateApplicationCommand[] diff --git a/ts/src/bot/botevent/types.ts b/ts/src/discord/botevent/types.ts similarity index 91% rename from ts/src/bot/botevent/types.ts rename to ts/src/discord/botevent/types.ts index 2bd9865..572b9c9 100644 --- a/ts/src/bot/botevent/types.ts +++ b/ts/src/discord/botevent/types.ts @@ -1,4 +1,4 @@ -import {BotType} from "#/bot" +import {BotType} from "#/discord" export type BotEventsType = BotType['events'] diff --git a/ts/src/bot/index.ts b/ts/src/discord/index.ts similarity index 54% rename from ts/src/bot/index.ts rename to ts/src/discord/index.ts index 570e79c..e84a324 100644 --- a/ts/src/bot/index.ts +++ b/ts/src/discord/index.ts @@ -1,10 +1,6 @@ -import { config } from "#/config"; -import { c } from "#/di"; -import { InjectionToken } from "@needle-di/core"; - -import {createBot, Intents} from "discordeno" - -const intents = [ +import { Intents, InteractionTypes } from "@discordeno/types"; +import type { Bot, DesiredPropertiesBehavior, CompleteDesiredProperties } from "discordeno"; +export const intents = [ Intents.GuildModeration , Intents.GuildWebhooks , Intents.GuildExpressions , @@ -22,9 +18,8 @@ const intents = [ Intents.GuildMessages, ] as const -export const createBotWithToken = (token: string) => createBot({ +export const createBotParameters = { intents: intents.reduce((acc, curr) => acc | curr, Intents.Guilds), - token: token, desiredProperties: { interaction: { id: true, @@ -45,18 +40,28 @@ export const createBotWithToken = (token: string) => createBot({ guildId: true, }, } -}) +} as const -export type BotType = ReturnType -export const Bot = new InjectionToken("DISCORD_BOT") -c.bind({ - provide: Bot, - async: true, - useFactory: async () => { - let token = config.DISCORD_TOKEN - if(!token) { - throw new Error('no discord token found. bot cant start'); - } - return createBotWithToken(token) - }, -}) +// Extract the type of desired properties from our parameters +type ExtractedDesiredProperties = typeof createBotParameters.desiredProperties; + +// The BotType uses the CompleteDesiredProperties helper to fill in the missing properties +export type BotType = Bot, DesiredPropertiesBehavior.RemoveKey>; + + +// Type for the interaction reference passed to workflows/activities +export interface InteractionRef { + id: bigint; + token: string; + type: InteractionTypes; + 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; +} diff --git a/ts/src/bot/mux/index.ts b/ts/src/discord/mux/index.ts similarity index 100% rename from ts/src/bot/mux/index.ts rename to ts/src/discord/mux/index.ts diff --git a/ts/src/payload-converter/adapter.ts b/ts/src/payload_converter/adapter.ts similarity index 100% rename from ts/src/payload-converter/adapter.ts rename to ts/src/payload_converter/adapter.ts diff --git a/ts/src/payload-converter/index.ts b/ts/src/payload_converter/index.ts similarity index 100% rename from ts/src/payload-converter/index.ts rename to ts/src/payload_converter/index.ts diff --git a/ts/src/services/temporal/index.ts b/ts/src/services/temporal/index.ts index 6abbcc1..bb62c70 100644 --- a/ts/src/services/temporal/index.ts +++ b/ts/src/services/temporal/index.ts @@ -13,7 +13,7 @@ c.bind({ connection, namespace: config.TEMPORAL_NAMESPACE, dataConverter: { - payloadConverterPath: require.resolve('../../payload-converter'), + payloadConverterPath: require.resolve('../../payload_converter'), }, }); process.on('exit', () => { diff --git a/ts/src/workflows/discord.ts b/ts/src/workflows/discord.ts index ceb5ff9..9d63dc3 100644 --- a/ts/src/workflows/discord.ts +++ b/ts/src/workflows/discord.ts @@ -1,119 +1,86 @@ import { proxyActivities, startChild, workflowInfo } from '@temporalio/workflow'; import type * as activities from '#/activities'; -import { ApplicationCommandOptionTypes, InteractionTypes } from 'discordeno'; -import { handleCommandGuildInfo, handleCommandGuildOnline, handleCommandGuildLeaderboard } from './guild-messages'; -import { BotType } from '#/bot'; +import { InteractionTypes } from '@discordeno/types'; +import { handleCommandGuildInfo, handleCommandGuildOnline, handleCommandGuildLeaderboard } from './guild_messages'; +import { createCommandHandler } from '#/discord/botevent/command_parser'; +import { SLASH_COMMANDS } from '#/discord/botevent/slash_commands'; +import { InteractionCreatePayload} from '#/discord'; const { reply_to_interaction } = proxyActivities({ startToCloseTimeout: '1 minute', }); - - -interface HandleInteractionCreatePayload { - ref: activities.InteractionRef - data: Parameters>[0]['data'] -} - - - +// Define command handlers with type safety const workflowHandleApplicationCommand = async ( - payload: HandleInteractionCreatePayload, + payload: InteractionCreatePayload, ) => { const { ref, data } = payload; + const notFoundHandler = async (content: string) => { + await reply_to_interaction({ + ref, + type: 4, + options: { + content: content, + isPrivate: true, + } + }); + } if (!data || !data.name) { - await reply_to_interaction({ - ref, - type: 4, - options: { - content: "Invalid command data", - isPrivate: true, - } - }); - return; + await notFoundHandler(`Invalid command data`); + return } - - // Build command path - const commandPath: string[] = [data.name]; - if (data.options) { - for (const option of data.options) { - if (option.type === ApplicationCommandOptionTypes.SubCommand) { - commandPath.push(option.name); - } + const commandHandler = createCommandHandler({ + notFoundHandler: async () => { + await notFoundHandler(`command not found`); + }, + handler:{ + guild: { + info: async (args: {}) => { + const { workflowId } = workflowInfo(); + const handle = await startChild(handleCommandGuildInfo, { + args: [{ ref }], + workflowId: `${workflowId}-guild-info`, + }); + await handle.result(); + }, + online: async (args: {}) => { + const { workflowId } = workflowInfo(); + const handle = await startChild(handleCommandGuildOnline, { + args: [{ ref }], + workflowId: `${workflowId}-guild-online`, + }); + await handle.result(); + }, + leaderboard: async (args: {}) => { + const { workflowId } = workflowInfo(); + const handle = await startChild(handleCommandGuildLeaderboard, { + args: [{ ref }], + workflowId: `${workflowId}-guild-leaderboard`, + }); + await handle.result(); + }, + }, + admin: { + set_wynn_guild: async (args: {}) => { + await reply_to_interaction({ + ref, + type: 4, + options: { + content: "Not implemented yet", + isPrivate: true, + } + }); + }, + }, } - } + }); - // Route to appropriate child workflow based on command path - const fullCommand = commandPath.join('.'); - const { workflowId } = workflowInfo(); - - try { - switch (fullCommand) { - case 'guild.info': { - const handle = await startChild(handleCommandGuildInfo, { - args: [{ ref }], - workflowId: `${workflowId}-guild-info`, - }); - await handle.result(); - break; - } - - case 'guild.online': { - const handle = await startChild(handleCommandGuildOnline, { - args: [{ ref }], - workflowId: `${workflowId}-guild-online`, - }); - await handle.result(); - break; - } - - case 'guild.leaderboard': { - const handle = await startChild(handleCommandGuildLeaderboard, { - args: [{ ref }], - workflowId: `${workflowId}-guild-leaderboard`, - }); - await handle.result(); - break; - } - - case 'admin.set_wynn_guild': { - await reply_to_interaction({ - ref, - type: 4, - options: { - content: "Not implemented yet", - isPrivate: true, - } - }); - break; - } - - default: { - await reply_to_interaction({ - ref, - type: 4, - options: { - content: `Command not implemented: ${fullCommand}`, - isPrivate: true, - } - }); - } - } - } catch (error) { - await reply_to_interaction({ - ref, - type: 4, - options: { - content: `Error executing command: ${error}`, - isPrivate: true, - } - }); - } + await commandHandler(data); } export const workflowHandleInteractionCreate = async ( - payload: HandleInteractionCreatePayload, + payload: InteractionCreatePayload, ) => { const {ref, data} = payload diff --git a/ts/src/workflows/guild-messages.ts b/ts/src/workflows/guild_messages.ts similarity index 94% rename from ts/src/workflows/guild-messages.ts rename to ts/src/workflows/guild_messages.ts index e081d8f..1c5f6de 100644 --- a/ts/src/workflows/guild-messages.ts +++ b/ts/src/workflows/guild_messages.ts @@ -1,6 +1,7 @@ import { proxyActivities } from "@temporalio/workflow"; import type * as activities from "#/activities"; import { WYNN_GUILD_ID } from "#/constants"; +import { InteractionRef } from "#/discord"; const { formGuildInfoMessage, @@ -12,7 +13,7 @@ const { }); interface CommandPayload { - ref: activities.InteractionRef; + ref: InteractionRef; } export async function handleCommandGuildInfo(payload: CommandPayload): Promise { diff --git a/ts/src/workflows/index.ts b/ts/src/workflows/index.ts index 690e795..c6a7c03 100644 --- a/ts/src/workflows/index.ts +++ b/ts/src/workflows/index.ts @@ -3,7 +3,7 @@ */ export * from "./discord"; -export * from "./guild-messages"; +export * from "./guild_messages"; export * from "./guilds"; export * from "./items"; export * from "./players"; diff --git a/ts/yarn.lock b/ts/yarn.lock index d2ec3f2..65b5556 100644 --- a/ts/yarn.lock +++ b/ts/yarn.lock @@ -74,7 +74,7 @@ __metadata: languageName: node linkType: hard -"@discordeno/types@npm:21.0.0": +"@discordeno/types@npm:21.0.0, @discordeno/types@npm:^21.0.0": version: 21.0.0 resolution: "@discordeno/types@npm:21.0.0" checksum: 10c0/5d47321e75cca4aa60f69d8b146c1c87fae4c4f7065a422a41ef17d34747b6d933d1699567a887beca5e6fc086b5cba0db9ca67b34b81f348afdd9432ea2979d @@ -1324,6 +1324,7 @@ __metadata: version: 0.0.0-use.local resolution: "backend@workspace:." dependencies: + "@discordeno/types": "npm:^21.0.0" "@needle-di/core": "npm:^0.10.1" "@temporalio/activity": "npm:^1.11.7" "@temporalio/client": "npm:^1.11.7"