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