noot
This commit is contained in:
parent
bc464c6185
commit
edb99b5c3b
@ -7,7 +7,10 @@
|
|||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"test:typecheck": "vitest typecheck",
|
"test:typecheck": "vitest typecheck",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"lint": "biome check .",
|
||||||
|
"lint:fix": "biome check --write .",
|
||||||
|
"format": "biome format --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
|
|||||||
@ -17,13 +17,13 @@ export async function update_wynn_items() {
|
|||||||
if (parsed instanceof ArkErrors) {
|
if (parsed instanceof ArkErrors) {
|
||||||
throw parsed
|
throw parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate we have a reasonable number of items
|
// Validate we have a reasonable number of items
|
||||||
const itemCount = Object.keys(parsed).length
|
const itemCount = Object.keys(parsed).length
|
||||||
if (itemCount < 100) {
|
if (itemCount < 100) {
|
||||||
throw new Error(`Received suspiciously low number of items: ${itemCount}. Refusing to update to prevent data loss.`)
|
throw new Error(`Received suspiciously low number of items: ${itemCount}. Refusing to update to prevent data loss.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sql } = await c.getAsync(PG)
|
const { sql } = await c.getAsync(PG)
|
||||||
// serialize the entire fullresult
|
// serialize the entire fullresult
|
||||||
const serializedData = stringify(parsed)
|
const serializedData = stringify(parsed)
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
|
import { InteractionResponseTypes } from '@discordeno/types'
|
||||||
|
import { ApplicationFailure } from '@temporalio/common'
|
||||||
import { type InteractionCallbackData, type InteractionCallbackOptions, MessageFlags } from 'discordeno'
|
import { type InteractionCallbackData, type InteractionCallbackOptions, MessageFlags } from 'discordeno'
|
||||||
import { c } from '#/di'
|
import { c } from '#/di'
|
||||||
import type { InteractionRef } from '#/discord'
|
import type { InteractionRef } from '#/discord'
|
||||||
import { InteractionResponseTypes } from '@discordeno/types'
|
|
||||||
import { Bot } from '#/discord/bot'
|
import { Bot } from '#/discord/bot'
|
||||||
import { ApplicationFailure } from '@temporalio/common'
|
|
||||||
import { logger } from '#/logger'
|
import { logger } from '#/logger'
|
||||||
|
|
||||||
const log = logger.child({ component: 'discord-activity' })
|
const log = logger.child({ component: 'discord-activity' })
|
||||||
@ -27,18 +27,16 @@ export const reply_to_interaction = async (props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (type) {
|
if (type === InteractionResponseTypes.UpdateMessage && ref.acknowledged) {
|
||||||
case InteractionResponseTypes.UpdateMessage:
|
return await bot.helpers.editOriginalInteractionResponse(ref.token, data)
|
||||||
if(ref.acknowledged) {
|
|
||||||
return await bot.helpers.editOriginalInteractionResponse(ref.token, data)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if (ref.acknowledged) {
|
|
||||||
const followUp = await bot.helpers.sendFollowupMessage(ref.token, data)
|
|
||||||
return followUp
|
|
||||||
}
|
|
||||||
return await bot.helpers.sendInteractionResponse(ref.id, ref.token, { type, data }, { withResponse: options?.withResponse })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ref.acknowledged) {
|
||||||
|
const followUp = await bot.helpers.sendFollowupMessage(ref.token, data)
|
||||||
|
return followUp
|
||||||
|
}
|
||||||
|
|
||||||
|
return await bot.helpers.sendInteractionResponse(ref.id, ref.token, { type, data }, { withResponse: options?.withResponse })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Check if it's a Discord API error
|
// Check if it's a Discord API error
|
||||||
if (error.status === 404 || error.code === 10062) {
|
if (error.status === 404 || error.code === 10062) {
|
||||||
@ -48,16 +46,13 @@ export const reply_to_interaction = async (props: {
|
|||||||
interactionId: ref.id,
|
interactionId: ref.id,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
code: error.code,
|
code: error.code,
|
||||||
status: error.status
|
status: error.status,
|
||||||
},
|
},
|
||||||
'Discord interaction expired (404) - not retrying'
|
'Discord interaction expired (404) - not retrying'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Throw non-retryable error to prevent Temporal from retrying
|
// Throw non-retryable error to prevent Temporal from retrying
|
||||||
throw ApplicationFailure.nonRetryable(
|
throw ApplicationFailure.nonRetryable(`Discord interaction ${ref.id} has expired and cannot be responded to`, 'DiscordInteractionExpired')
|
||||||
`Discord interaction ${ref.id} has expired and cannot be responded to`,
|
|
||||||
'DiscordInteractionExpired'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log other errors and re-throw them (these will be retried by Temporal)
|
// Log other errors and re-throw them (these will be retried by Temporal)
|
||||||
@ -67,7 +62,7 @@ export const reply_to_interaction = async (props: {
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
code: error.code,
|
code: error.code,
|
||||||
status: error.status
|
status: error.status,
|
||||||
},
|
},
|
||||||
'Failed to reply to Discord interaction'
|
'Failed to reply to Discord interaction'
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { c } from '#/di'
|
import { c } from '#/di'
|
||||||
import { PG } from '#/services/pg'
|
|
||||||
import { DiscordGuildSettingsService, DiscordGuildSettingKey, DiscordGuildSettingValue } from '#/services/guild_settings'
|
|
||||||
import { logger } from '#/logger'
|
import { logger } from '#/logger'
|
||||||
|
import { type DiscordGuildSettingKey, type DiscordGuildSettingValue, DiscordGuildSettingsService } from '#/services/guild_settings'
|
||||||
|
import { PG } from '#/services/pg'
|
||||||
|
|
||||||
export interface SetDiscordGuildSettingParams<K extends DiscordGuildSettingKey> {
|
export interface SetDiscordGuildSettingParams<K extends DiscordGuildSettingKey> {
|
||||||
guildId: bigint
|
guildId: bigint
|
||||||
@ -9,32 +9,27 @@ export interface SetDiscordGuildSettingParams<K extends DiscordGuildSettingKey>
|
|||||||
value: DiscordGuildSettingValue<K>
|
value: DiscordGuildSettingValue<K>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function set_discord_guild_setting<K extends DiscordGuildSettingKey>(
|
export async function set_discord_guild_setting<K extends DiscordGuildSettingKey>(params: SetDiscordGuildSettingParams<K>): Promise<void> {
|
||||||
params: SetDiscordGuildSettingParams<K>
|
|
||||||
): Promise<void> {
|
|
||||||
const settingsService = await c.getAsync(DiscordGuildSettingsService)
|
const settingsService = await c.getAsync(DiscordGuildSettingsService)
|
||||||
|
|
||||||
await settingsService.setSetting(params.guildId, params.key, params.value)
|
await settingsService.setSetting(params.guildId, params.key, params.value)
|
||||||
|
|
||||||
logger.info({
|
logger.info(
|
||||||
guildId: params.guildId,
|
{
|
||||||
key: params.key,
|
guildId: params.guildId,
|
||||||
}, 'discord guild setting updated')
|
key: params.key,
|
||||||
|
},
|
||||||
|
'discord guild setting updated'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_discord_guild_setting<K extends DiscordGuildSettingKey>(
|
export async function get_discord_guild_setting<K extends DiscordGuildSettingKey>(guildId: bigint, key: K): Promise<DiscordGuildSettingValue<K> | null> {
|
||||||
guildId: bigint,
|
|
||||||
key: K
|
|
||||||
): Promise<DiscordGuildSettingValue<K> | null> {
|
|
||||||
const settingsService = await c.getAsync(DiscordGuildSettingsService)
|
const settingsService = await c.getAsync(DiscordGuildSettingsService)
|
||||||
|
|
||||||
return await settingsService.getSetting(guildId, key)
|
return await settingsService.getSetting(guildId, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function delete_discord_guild_setting(
|
export async function delete_discord_guild_setting(guildId: bigint, key: DiscordGuildSettingKey): Promise<boolean> {
|
||||||
guildId: bigint,
|
|
||||||
key: DiscordGuildSettingKey
|
|
||||||
): Promise<boolean> {
|
|
||||||
const settingsService = await c.getAsync(DiscordGuildSettingsService)
|
const settingsService = await c.getAsync(DiscordGuildSettingsService)
|
||||||
|
|
||||||
return await settingsService.deleteSetting(guildId, key)
|
return await settingsService.deleteSetting(guildId, key)
|
||||||
@ -58,4 +53,3 @@ export async function get_wynn_guild_info(guildNameOrTag: string): Promise<{ uid
|
|||||||
|
|
||||||
return result[0]
|
return result[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,19 +16,16 @@ const playerSchemaSuccess = type({
|
|||||||
|
|
||||||
const playerSchema = playerSchemaFail.or(playerSchemaSuccess)
|
const playerSchema = playerSchemaFail.or(playerSchemaSuccess)
|
||||||
|
|
||||||
|
|
||||||
const getUUIDForUsername = async (username: string) => {
|
const getUUIDForUsername = async (username: string) => {
|
||||||
const resp = await axios.get(`https://api.mojang.com/users/profiles/minecraft/${username}`, {
|
const resp = await axios.get(`https://api.mojang.com/users/profiles/minecraft/${username}`, {
|
||||||
validateStatus: function (status) {
|
validateStatus: (status) => status < 300 && status >= 200,
|
||||||
return status < 300 && status >= 200;
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
if (resp.headers['content-type'] !== 'application/json') {
|
if (resp.headers['content-type'] !== 'application/json') {
|
||||||
throw new Error(`invalid content type: ${resp.headers['content-type']}`)
|
throw new Error(`invalid content type: ${resp.headers['content-type']}`)
|
||||||
}
|
}
|
||||||
log.info('response data', {data: resp.data})
|
log.info('response data', { data: resp.data })
|
||||||
const parsed = playerSchema.assert(resp.data)
|
const parsed = playerSchema.assert(resp.data)
|
||||||
if('errorMessage' in parsed) {
|
if ('errorMessage' in parsed) {
|
||||||
throw new Error(`error message: ${parsed.errorMessage}`)
|
throw new Error(`error message: ${parsed.errorMessage}`)
|
||||||
}
|
}
|
||||||
const pid = parsed.id
|
const pid = parsed.id
|
||||||
|
|||||||
@ -19,8 +19,12 @@ export class BotCommand extends Command {
|
|||||||
const bot = await c.getAsync(Bot)
|
const bot = await c.getAsync(Bot)
|
||||||
bot.events = events()
|
bot.events = events()
|
||||||
logger.info('registering slash commands')
|
logger.info('registering slash commands')
|
||||||
await bot.rest.upsertGuildApplicationCommands(DISCORD_GUILD_ID, SLASH_COMMANDS).catch((err) => logger.error(err, 'failed to register slash commands for main guild'))
|
await bot.rest
|
||||||
await bot.rest.upsertGuildApplicationCommands('547828454972850196', SLASH_COMMANDS).catch((err) => logger.error(err, 'failed to register slash commands for secondary guild'))
|
.upsertGuildApplicationCommands(DISCORD_GUILD_ID, SLASH_COMMANDS)
|
||||||
|
.catch((err) => logger.error(err, 'failed to register slash commands for main guild'))
|
||||||
|
await bot.rest
|
||||||
|
.upsertGuildApplicationCommands('547828454972850196', SLASH_COMMANDS)
|
||||||
|
.catch((err) => logger.error(err, 'failed to register slash commands for secondary guild'))
|
||||||
|
|
||||||
logger.info('connecting bot to gateway')
|
logger.info('connecting bot to gateway')
|
||||||
await bot.start()
|
await bot.start()
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import { c } from '#/di'
|
|||||||
import '#/services/temporal'
|
import '#/services/temporal'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { Client, ScheduleNotFoundError, type ScheduleOptions, ScheduleOverlapPolicy } from '@temporalio/client'
|
import { Client, ScheduleNotFoundError, type ScheduleOptions, ScheduleOverlapPolicy } from '@temporalio/client'
|
||||||
import { NativeConnection, Worker, Runtime } from '@temporalio/worker'
|
import { NativeConnection, Runtime, Worker } from '@temporalio/worker'
|
||||||
import { PG } from '#/services/pg'
|
import { PG } from '#/services/pg'
|
||||||
import { workflowSyncAllGuilds, workflowSyncGuildLeaderboardInfo, workflowSyncGuilds, workflowSyncOnline, workflowSyncItemDatabase } from '#/workflows'
|
import { workflowSyncAllGuilds, workflowSyncGuildLeaderboardInfo, workflowSyncGuilds, workflowSyncItemDatabase, workflowSyncOnline } from '#/workflows'
|
||||||
import * as activities from '../activities'
|
import * as activities from '../activities'
|
||||||
|
|
||||||
import { config } from '#/config'
|
import { config } from '#/config'
|
||||||
@ -135,7 +135,7 @@ export class WorkerCommand extends Command {
|
|||||||
log: (level, message, attrs) => {
|
log: (level, message, attrs) => {
|
||||||
const pinoLevel = level.toLowerCase()
|
const pinoLevel = level.toLowerCase()
|
||||||
if (pinoLevel in logger) {
|
if (pinoLevel in logger) {
|
||||||
(logger as any)[pinoLevel](attrs, message)
|
;(logger as any)[pinoLevel](attrs, message)
|
||||||
} else {
|
} else {
|
||||||
logger.info(attrs, message)
|
logger.info(attrs, message)
|
||||||
}
|
}
|
||||||
@ -180,11 +180,6 @@ export class WorkerCommand extends Command {
|
|||||||
taskQueue: 'wynn-worker-ts',
|
taskQueue: 'wynn-worker-ts',
|
||||||
stickyQueueScheduleToStartTimeout: 5 * 1000,
|
stickyQueueScheduleToStartTimeout: 5 * 1000,
|
||||||
activities,
|
activities,
|
||||||
defaultActivityOptions: {
|
|
||||||
retry: {
|
|
||||||
maximumAttempts: 30,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
logger.info({ taskQueue: 'wynn-worker-ts', namespace: config.TEMPORAL_NAMESPACE }, 'starting temporal worker')
|
logger.info({ taskQueue: 'wynn-worker-ts', namespace: config.TEMPORAL_NAMESPACE }, 'starting temporal worker')
|
||||||
await worker.run()
|
await worker.run()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, type CreateApplicationCommand } from '@discordeno/types'
|
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, type CreateApplicationCommand } from '@discordeno/types'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
|
||||||
import type { InteractionData } from 'discordeno'
|
import type { InteractionData } from 'discordeno'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { type ExtractCommands, createCommandHandler } from './command_parser'
|
import { type ExtractCommands, createCommandHandler } from './command_parser'
|
||||||
|
|
||||||
// Test command definitions
|
// Test command definitions
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { ApplicationCommandOptionTypes } from '@discordeno/types'
|
import { ApplicationCommandOptionTypes } from '@discordeno/types'
|
||||||
import type {CreateApplicationCommand,
|
import type { CreateApplicationCommand, DiscordApplicationCommandOption, DiscordInteractionDataOption } from '@discordeno/types'
|
||||||
DiscordInteractionDataOption,
|
import type { InteractionData } from 'discordeno'
|
||||||
DiscordApplicationCommandOption
|
|
||||||
}from '@discordeno/types'
|
|
||||||
import type {InteractionData} from 'discordeno'
|
|
||||||
import type { SLASH_COMMANDS } from './slash_commands'
|
import type { SLASH_COMMANDS } from './slash_commands'
|
||||||
|
|
||||||
// Map option types to their TypeScript types
|
// Map option types to their TypeScript types
|
||||||
@ -20,56 +17,67 @@ type OptionTypeMap = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper type to get option by name
|
// Helper type to get option by name
|
||||||
type GetOption<Options, Name> = Options extends readonly DiscordApplicationCommandOption[] ? (Options[number] extends infer O ? (O extends { name: Name } ? O : never) : never) : never
|
type GetOption<Options, Name> = Options extends readonly DiscordApplicationCommandOption[]
|
||||||
|
? Options[number] extends infer O
|
||||||
|
? O extends { name: Name }
|
||||||
|
? O
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
// Extract the argument types from command options
|
// Extract the argument types from command options
|
||||||
export type ExtractArgs<Options extends readonly DiscordApplicationCommandOption[]> = {
|
export type ExtractArgs<Options extends readonly DiscordApplicationCommandOption[]> = {
|
||||||
[K in Options[number]['name']]: GetOption<Options, K> extends { type: infer T; required?: infer R }
|
[K in Options[number]['name']]: GetOption<Options, K> extends { type: infer T; required?: infer R }
|
||||||
? T extends keyof OptionTypeMap
|
? T extends keyof OptionTypeMap
|
||||||
? R extends true
|
? R extends true
|
||||||
? OptionTypeMap[T]
|
? OptionTypeMap[T]
|
||||||
: OptionTypeMap[T] | undefined
|
: OptionTypeMap[T] | undefined
|
||||||
: never
|
: never
|
||||||
: never
|
: never
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler function type that accepts typed arguments
|
// Handler function type that accepts typed arguments
|
||||||
type HandlerFunction<Args = Record<string, never>> = (args: Args) => Promise<void> | void
|
type HandlerFunction<Args = Record<string, never>> = (args: Args) => Promise<void> | void
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Get subcommand or subcommand group by name
|
// Get subcommand or subcommand group by name
|
||||||
type GetSubcommandOrGroup<Options, Name> = Options extends readonly DiscordApplicationCommandOption[]
|
type GetSubcommandOrGroup<Options, Name> = Options extends readonly DiscordApplicationCommandOption[]
|
||||||
? Options[number] extends infer O
|
? Options[number] extends infer O
|
||||||
? O extends { name: Name; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup }
|
? O extends { name: Name; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup }
|
||||||
? O
|
? O
|
||||||
: never
|
: never
|
||||||
: never
|
: never
|
||||||
: never
|
: never
|
||||||
|
|
||||||
// Check if all options are subcommands or subcommand groups
|
// Check if all options are subcommands or subcommand groups
|
||||||
type HasOnlySubcommands<Options extends readonly DiscordApplicationCommandOption[]> = Options[number] extends { type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup } ? true : false
|
type HasOnlySubcommands<Options extends readonly DiscordApplicationCommandOption[]> = Options[number] extends {
|
||||||
|
type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup
|
||||||
|
}
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
|
||||||
// Extract subcommand or subcommand group names from options
|
// Extract subcommand or subcommand group names from options
|
||||||
type SubcommandNames<Options extends readonly DiscordApplicationCommandOption[]> = Options[number] extends { name: infer N; type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup }
|
type SubcommandNames<Options extends readonly DiscordApplicationCommandOption[]> = Options[number] extends {
|
||||||
? N extends string
|
name: infer N
|
||||||
? N
|
type: ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup
|
||||||
: never
|
}
|
||||||
: never
|
? N extends string
|
||||||
|
? N
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
// Type to extract subcommand handlers (recursive for groups)
|
// Type to extract subcommand handlers (recursive for groups)
|
||||||
export type SubcommandHandlers<Options extends readonly DiscordApplicationCommandOption[]> = {
|
export type SubcommandHandlers<Options extends readonly DiscordApplicationCommandOption[]> = {
|
||||||
[K in SubcommandNames<Options>]: GetSubcommandOrGroup<Options, K> extends { type: infer T; options?: infer SubOpts }
|
[K in SubcommandNames<Options>]: GetSubcommandOrGroup<Options, K> extends { type: infer T; options?: infer SubOpts }
|
||||||
? T extends ApplicationCommandOptionTypes.SubCommandGroup
|
? T extends ApplicationCommandOptionTypes.SubCommandGroup
|
||||||
? SubOpts extends readonly DiscordApplicationCommandOption[]
|
? SubOpts extends readonly DiscordApplicationCommandOption[]
|
||||||
? SubcommandHandlers<SubOpts>
|
? SubcommandHandlers<SubOpts>
|
||||||
: never
|
: never
|
||||||
: T extends ApplicationCommandOptionTypes.SubCommand
|
: T extends ApplicationCommandOptionTypes.SubCommand
|
||||||
? SubOpts extends readonly DiscordApplicationCommandOption[]
|
? SubOpts extends readonly DiscordApplicationCommandOption[]
|
||||||
? HandlerFunction<ExtractArgs<SubOpts>>
|
? HandlerFunction<ExtractArgs<SubOpts>>
|
||||||
: HandlerFunction<Record<string, never>>
|
: HandlerFunction<Record<string, never>>
|
||||||
: never
|
: never
|
||||||
: never
|
: never
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get command by name from array
|
// Get command by name from array
|
||||||
@ -78,12 +86,12 @@ type GetCommand<Commands extends readonly any[], Name> = Commands[number] extend
|
|||||||
// Main type to extract command handlers from slash commands
|
// Main type to extract command handlers from slash commands
|
||||||
export type ExtractCommands<T extends readonly CreateApplicationCommand[]> = {
|
export type ExtractCommands<T extends readonly CreateApplicationCommand[]> = {
|
||||||
[Name in T[number]['name']]: GetCommand<T, Name> extends { options?: infer Options }
|
[Name in T[number]['name']]: GetCommand<T, Name> extends { options?: infer Options }
|
||||||
? Options extends readonly DiscordApplicationCommandOption[]
|
? Options extends readonly DiscordApplicationCommandOption[]
|
||||||
? HasOnlySubcommands<Options> extends true
|
? HasOnlySubcommands<Options> extends true
|
||||||
? SubcommandHandlers<Options>
|
? SubcommandHandlers<Options>
|
||||||
: HandlerFunction<ExtractArgs<Options>>
|
: HandlerFunction<ExtractArgs<Options>>
|
||||||
: HandlerFunction<Record<string, never>>
|
: HandlerFunction<Record<string, never>>
|
||||||
: HandlerFunction<Record<string, never>>
|
: HandlerFunction<Record<string, never>>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type representing the possible output of ExtractCommands
|
// Type representing the possible output of ExtractCommands
|
||||||
@ -119,7 +127,7 @@ function parseOptions<T extends DiscordInteractionDataOption[]>(options?: T): an
|
|||||||
async function handleCommands(
|
async function handleCommands(
|
||||||
handler: CommandHandler,
|
handler: CommandHandler,
|
||||||
options: DiscordInteractionDataOption[] | undefined,
|
options: DiscordInteractionDataOption[] | undefined,
|
||||||
notFoundHandler: HandlerFunction<{path?: string}>
|
notFoundHandler: HandlerFunction<{ path?: string }>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// If handler is a function, execute it with parsed options
|
// If handler is a function, execute it with parsed options
|
||||||
if (typeof handler === 'function') {
|
if (typeof handler === 'function') {
|
||||||
@ -129,9 +137,7 @@ async function handleCommands(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handler is a map of subcommands/groups
|
// Handler is a map of subcommands/groups
|
||||||
const subcommand = options?.find(
|
const subcommand = options?.find((opt) => opt.type === ApplicationCommandOptionTypes.SubCommand || opt.type === ApplicationCommandOptionTypes.SubCommandGroup)
|
||||||
(opt) => opt.type === ApplicationCommandOptionTypes.SubCommand || opt.type === ApplicationCommandOptionTypes.SubCommandGroup
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!subcommand) {
|
if (!subcommand) {
|
||||||
await notFoundHandler({})
|
await notFoundHandler({})
|
||||||
@ -153,10 +159,10 @@ export function createCommandHandler<T extends readonly CreateApplicationCommand
|
|||||||
handler,
|
handler,
|
||||||
notFoundHandler,
|
notFoundHandler,
|
||||||
}: {
|
}: {
|
||||||
commands: T,
|
commands: T
|
||||||
handler: ExtractCommands<T>
|
handler: ExtractCommands<T>
|
||||||
notFoundHandler: HandlerFunction<{path?: string}>
|
notFoundHandler: HandlerFunction<{ path?: string }>
|
||||||
}) {
|
}) {
|
||||||
return async (data: InteractionData): Promise<void> => {
|
return async (data: InteractionData): Promise<void> => {
|
||||||
if (!data || !data.name) {
|
if (!data || !data.name) {
|
||||||
await notFoundHandler({})
|
await notFoundHandler({})
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Client } from '@temporalio/client'
|
import { Client } from '@temporalio/client'
|
||||||
import { ActivityTypes, InteractionData, InteractionTypes } from 'discordeno'
|
import { ActivityTypes, type InteractionData, InteractionTypes } from 'discordeno'
|
||||||
import { c } from '#/di'
|
import { c } from '#/di'
|
||||||
import type { BotType } from '#/discord'
|
import type { BotType } from '#/discord'
|
||||||
import { Bot } from '#/discord/bot'
|
import { Bot } from '#/discord/bot'
|
||||||
@ -20,7 +20,7 @@ export const events = () => {
|
|||||||
|
|
||||||
const temporalClient = await c.getAsync(Client)
|
const temporalClient = await c.getAsync(Client)
|
||||||
|
|
||||||
let data: Omit<InteractionData, 'resolved'> = interaction.data
|
const data: Omit<InteractionData, 'resolved'> = interaction.data
|
||||||
|
|
||||||
// Start the workflow to handle the interaction
|
// Start the workflow to handle the interaction
|
||||||
const handle = await temporalClient.workflow.start(workflowHandleInteractionCreate, {
|
const handle = await temporalClient.workflow.start(workflowHandleInteractionCreate, {
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import { Intents, type InteractionTypes } from '@discordeno/types'
|
import { Intents, type InteractionTypes } from '@discordeno/types'
|
||||||
import type { Bot, CompleteDesiredProperties, DesiredPropertiesBehavior, DiscordInteractionContextType, InteractionData, RecursivePartial, TransformersDesiredProperties } from 'discordeno'
|
import type {
|
||||||
|
Bot,
|
||||||
|
CompleteDesiredProperties,
|
||||||
|
DesiredPropertiesBehavior,
|
||||||
|
DiscordInteractionContextType,
|
||||||
|
InteractionData,
|
||||||
|
RecursivePartial,
|
||||||
|
TransformersDesiredProperties,
|
||||||
|
} from 'discordeno'
|
||||||
export const intents = [
|
export const intents = [
|
||||||
Intents.GuildModeration,
|
Intents.GuildModeration,
|
||||||
Intents.GuildWebhooks,
|
Intents.GuildWebhooks,
|
||||||
@ -18,7 +26,6 @@ export const intents = [
|
|||||||
Intents.GuildMessages,
|
Intents.GuildMessages,
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
|
||||||
export const desiredProperties = {
|
export const desiredProperties = {
|
||||||
interaction: {
|
interaction: {
|
||||||
id: true,
|
id: true,
|
||||||
@ -59,22 +66,22 @@ export interface InteractionRef {
|
|||||||
type: InteractionTypes
|
type: InteractionTypes
|
||||||
acknowledged?: boolean
|
acknowledged?: boolean
|
||||||
/** The guild it was sent from */
|
/** The guild it was sent from */
|
||||||
guildId?: bigint;
|
guildId?: bigint
|
||||||
channelId?: bigint;
|
channelId?: bigint
|
||||||
/** Guild member data for the invoking user, including permissions */
|
/** Guild member data for the invoking user, including permissions */
|
||||||
memberId?: bigint ;
|
memberId?: bigint
|
||||||
/** User object for the invoking user, if invoked in a DM */
|
/** User object for the invoking user, if invoked in a DM */
|
||||||
userId?: bigint;
|
userId?: bigint
|
||||||
/** For the message the button was attached to */
|
/** For the message the button was attached to */
|
||||||
messageId?: bigint;
|
messageId?: bigint
|
||||||
|
|
||||||
// locale of the interaction
|
// locale of the interaction
|
||||||
locale?: string;
|
locale?: string
|
||||||
/** The guild's preferred locale, if invoked in a guild */
|
/** The guild's preferred locale, if invoked in a guild */
|
||||||
guildLocale?: string;
|
guildLocale?: string
|
||||||
|
|
||||||
/** Context where the interaction was triggered from */
|
/** Context where the interaction was triggered from */
|
||||||
context?: DiscordInteractionContextType;
|
context?: DiscordInteractionContextType
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type for the complete interaction handling payload
|
// Type for the complete interaction handling payload
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { type } from 'arktype'
|
|
||||||
import { c } from '#/di'
|
|
||||||
import { PG } from '#/services/pg'
|
|
||||||
import { logger } from '#/logger'
|
|
||||||
import { JSONValue } from 'postgres'
|
|
||||||
import { snowflake } from '#/utils/types'
|
|
||||||
import { inject } from '@needle-di/core'
|
import { inject } from '@needle-di/core'
|
||||||
|
import { type } from 'arktype'
|
||||||
|
import type { JSONValue } from 'postgres'
|
||||||
|
import { c } from '#/di'
|
||||||
|
import { logger } from '#/logger'
|
||||||
|
import { PG } from '#/services/pg'
|
||||||
|
import { snowflake } from '#/utils/types'
|
||||||
|
|
||||||
// Define the guild settings types
|
// Define the guild settings types
|
||||||
export const DiscordGuildSettingSchema = type({
|
export const DiscordGuildSettingSchema = type({
|
||||||
@ -15,7 +15,6 @@ export const DiscordGuildSettingSchema = type({
|
|||||||
|
|
||||||
export type DiscordGuildSetting = typeof DiscordGuildSettingSchema.infer
|
export type DiscordGuildSetting = typeof DiscordGuildSettingSchema.infer
|
||||||
|
|
||||||
|
|
||||||
// Define setting schemas with arktype
|
// Define setting schemas with arktype
|
||||||
export const DISCORD_GUILD_SETTING_SCHEMAS = {
|
export const DISCORD_GUILD_SETTING_SCHEMAS = {
|
||||||
wynn_guild: type('string.uuid'),
|
wynn_guild: type('string.uuid'),
|
||||||
@ -37,8 +36,7 @@ export const DISCORD_GUILD_SETTING_SCHEMAS = {
|
|||||||
|
|
||||||
export type DiscordGuildSettingKey = keyof typeof DISCORD_GUILD_SETTING_SCHEMAS
|
export type DiscordGuildSettingKey = keyof typeof DISCORD_GUILD_SETTING_SCHEMAS
|
||||||
|
|
||||||
export type DiscordGuildSettingValue<K extends DiscordGuildSettingKey> =
|
export type DiscordGuildSettingValue<K extends DiscordGuildSettingKey> = (typeof DISCORD_GUILD_SETTING_SCHEMAS)[K]['infer']
|
||||||
typeof DISCORD_GUILD_SETTING_SCHEMAS[K]['infer']
|
|
||||||
|
|
||||||
export class DiscordGuildSettingsService {
|
export class DiscordGuildSettingsService {
|
||||||
constructor(private readonly pg = inject(PG)) {}
|
constructor(private readonly pg = inject(PG)) {}
|
||||||
@ -46,10 +44,7 @@ export class DiscordGuildSettingsService {
|
|||||||
/**
|
/**
|
||||||
* Get a specific setting for a guild
|
* Get a specific setting for a guild
|
||||||
*/
|
*/
|
||||||
async getSetting<K extends DiscordGuildSettingKey>(
|
async getSetting<K extends DiscordGuildSettingKey>(guildId: bigint, key: K): Promise<DiscordGuildSettingValue<K> | null> {
|
||||||
guildId: bigint,
|
|
||||||
key: K
|
|
||||||
): Promise<DiscordGuildSettingValue<K> | null> {
|
|
||||||
const { sql } = this.pg
|
const { sql } = this.pg
|
||||||
|
|
||||||
const result = await sql<{ value: unknown }[]>`
|
const result = await sql<{ value: unknown }[]>`
|
||||||
@ -84,17 +79,13 @@ export class DiscordGuildSettingsService {
|
|||||||
WHERE guild_id = ${guildId.toString()}
|
WHERE guild_id = ${guildId.toString()}
|
||||||
ORDER BY key
|
ORDER BY key
|
||||||
`
|
`
|
||||||
return result.map(row => DiscordGuildSettingSchema.assert(row))
|
return result.map((row) => DiscordGuildSettingSchema.assert(row))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a setting for a guild (upsert)
|
* Set a setting for a guild (upsert)
|
||||||
*/
|
*/
|
||||||
async setSetting<K extends DiscordGuildSettingKey>(
|
async setSetting<K extends DiscordGuildSettingKey>(guildId: bigint, key: K, value: DiscordGuildSettingValue<K>): Promise<void> {
|
||||||
guildId: bigint,
|
|
||||||
key: K,
|
|
||||||
value: DiscordGuildSettingValue<K>
|
|
||||||
): Promise<void> {
|
|
||||||
const { sql } = this.pg
|
const { sql } = this.pg
|
||||||
|
|
||||||
// Validate the value with arktype
|
// Validate the value with arktype
|
||||||
@ -155,8 +146,6 @@ export class DiscordGuildSettingsService {
|
|||||||
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the service with dependency injection
|
// Register the service with dependency injection
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export * from './snowflake'
|
export * from './snowflake'
|
||||||
|
|||||||
@ -12,4 +12,4 @@ export const snowflake = type('string|bigint').pipe((value, ctx) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Snowflake = bigint
|
export type Snowflake = bigint
|
||||||
|
|||||||
@ -76,10 +76,12 @@ const workflowHandleApplicationCommand = async (payload: InteractionCreatePayloa
|
|||||||
set_wynn_guild: async (args) => {
|
set_wynn_guild: async (args) => {
|
||||||
const handle = await startChild(handleCommandSetWynnGuild, {
|
const handle = await startChild(handleCommandSetWynnGuild, {
|
||||||
workflowId: `${workflowInfo().workflowId}-set-wynn-guild`,
|
workflowId: `${workflowInfo().workflowId}-set-wynn-guild`,
|
||||||
args: [{
|
args: [
|
||||||
ref,
|
{
|
||||||
args,
|
ref,
|
||||||
}],
|
args,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
await handle.result()
|
await handle.result()
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import type * as activities from '#/activities'
|
|||||||
import { WYNN_GUILD_ID } from '#/constants'
|
import { WYNN_GUILD_ID } from '#/constants'
|
||||||
import type { InteractionRef } from '#/discord'
|
import type { InteractionRef } from '#/discord'
|
||||||
|
|
||||||
const { formGuildInfoMessage, formGuildOnlineMessage, formGuildLeaderboardMessage, reply_to_interaction, get_discord_guild_setting } = proxyActivities<typeof activities>({
|
const { formGuildInfoMessage, formGuildOnlineMessage, formGuildLeaderboardMessage, reply_to_interaction, get_discord_guild_setting } = proxyActivities<
|
||||||
|
typeof activities
|
||||||
|
>({
|
||||||
startToCloseTimeout: '30 seconds',
|
startToCloseTimeout: '30 seconds',
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -14,7 +16,7 @@ interface CommandPayload {
|
|||||||
|
|
||||||
export async function handleCommandGuildInfo(payload: CommandPayload): Promise<void> {
|
export async function handleCommandGuildInfo(payload: CommandPayload): Promise<void> {
|
||||||
const { ref, discordGuildId } = payload
|
const { ref, discordGuildId } = payload
|
||||||
|
|
||||||
// Try to get the associated Wynncraft guild for this Discord guild
|
// Try to get the associated Wynncraft guild for this Discord guild
|
||||||
let guildId = WYNN_GUILD_ID // Default fallback
|
let guildId = WYNN_GUILD_ID // Default fallback
|
||||||
if (discordGuildId) {
|
if (discordGuildId) {
|
||||||
@ -23,7 +25,7 @@ export async function handleCommandGuildInfo(payload: CommandPayload): Promise<v
|
|||||||
guildId = wynnGuild
|
guildId = wynnGuild
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = await formGuildInfoMessage(guildId)
|
const msg = await formGuildInfoMessage(guildId)
|
||||||
await reply_to_interaction({
|
await reply_to_interaction({
|
||||||
ref,
|
ref,
|
||||||
@ -34,7 +36,7 @@ export async function handleCommandGuildInfo(payload: CommandPayload): Promise<v
|
|||||||
|
|
||||||
export async function handleCommandGuildOnline(payload: CommandPayload): Promise<void> {
|
export async function handleCommandGuildOnline(payload: CommandPayload): Promise<void> {
|
||||||
const { ref, discordGuildId } = payload
|
const { ref, discordGuildId } = payload
|
||||||
|
|
||||||
// Try to get the associated Wynncraft guild for this Discord guild
|
// Try to get the associated Wynncraft guild for this Discord guild
|
||||||
let guildId = WYNN_GUILD_ID // Default fallback
|
let guildId = WYNN_GUILD_ID // Default fallback
|
||||||
if (discordGuildId) {
|
if (discordGuildId) {
|
||||||
@ -43,7 +45,7 @@ export async function handleCommandGuildOnline(payload: CommandPayload): Promise
|
|||||||
guildId = wynnGuild
|
guildId = wynnGuild
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = await formGuildOnlineMessage(guildId)
|
const msg = await formGuildOnlineMessage(guildId)
|
||||||
await reply_to_interaction({
|
await reply_to_interaction({
|
||||||
ref,
|
ref,
|
||||||
@ -54,7 +56,7 @@ export async function handleCommandGuildOnline(payload: CommandPayload): Promise
|
|||||||
|
|
||||||
export async function handleCommandGuildLeaderboard(payload: CommandPayload): Promise<void> {
|
export async function handleCommandGuildLeaderboard(payload: CommandPayload): Promise<void> {
|
||||||
const { ref, discordGuildId } = payload
|
const { ref, discordGuildId } = payload
|
||||||
|
|
||||||
// Try to get the associated Wynncraft guild for this Discord guild
|
// Try to get the associated Wynncraft guild for this Discord guild
|
||||||
let guildId = WYNN_GUILD_ID // Default fallback
|
let guildId = WYNN_GUILD_ID // Default fallback
|
||||||
if (discordGuildId) {
|
if (discordGuildId) {
|
||||||
@ -63,7 +65,7 @@ export async function handleCommandGuildLeaderboard(payload: CommandPayload): Pr
|
|||||||
guildId = wynnGuild
|
guildId = wynnGuild
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = await formGuildLeaderboardMessage(guildId)
|
const msg = await formGuildLeaderboardMessage(guildId)
|
||||||
await reply_to_interaction({
|
await reply_to_interaction({
|
||||||
ref,
|
ref,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
import { InteractionResponseTypes } from '@discordeno/types'
|
||||||
import { proxyActivities } from '@temporalio/workflow'
|
import { proxyActivities } from '@temporalio/workflow'
|
||||||
import type * as activities from '#/activities'
|
import type * as activities from '#/activities'
|
||||||
import type { InteractionRef } from '#/discord'
|
import type { InteractionRef } from '#/discord'
|
||||||
import { InteractionResponseTypes } from '@discordeno/types'
|
|
||||||
|
|
||||||
const { reply_to_interaction, set_discord_guild_setting, get_wynn_guild_info } = proxyActivities<typeof activities>({
|
const { reply_to_interaction, set_discord_guild_setting, get_wynn_guild_info } = proxyActivities<typeof activities>({
|
||||||
startToCloseTimeout: '10 seconds',
|
startToCloseTimeout: '10 seconds',
|
||||||
@ -24,13 +24,12 @@ export async function handleCommandSetWynnGuild(payload: SetWynnGuildPayload): P
|
|||||||
})
|
})
|
||||||
ref.acknowledged = true
|
ref.acknowledged = true
|
||||||
|
|
||||||
|
|
||||||
if (!ref.guildId) {
|
if (!ref.guildId) {
|
||||||
await reply_to_interaction({
|
await reply_to_interaction({
|
||||||
ref,
|
ref,
|
||||||
type: InteractionResponseTypes.UpdateMessage,
|
type: InteractionResponseTypes.UpdateMessage,
|
||||||
options: {
|
options: {
|
||||||
content: `❌ Could not find discord guild. Please try again.`,
|
content: '❌ Could not find discord guild. Please try again.',
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user