noot
All checks were successful
commit-tag / commit-tag-image (map[context:./migrations file:./migrations/Dockerfile name:migrations]) (push) Successful in 18s
commit-tag / commit-tag-image (map[context:./ts file:./ts/Dockerfile name:ts]) (push) Successful in 1m36s

This commit is contained in:
a 2025-07-15 23:34:47 -05:00
parent bc464c6185
commit edb99b5c3b
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
17 changed files with 149 additions and 156 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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'
) )

View File

@ -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]
} }

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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({})

View File

@ -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, {

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
export * from './snowflake' export * from './snowflake'

View File

@ -12,4 +12,4 @@ export const snowflake = type('string|bigint').pipe((value, ctx) => {
} }
}) })
export type Snowflake = bigint export type Snowflake = bigint

View File

@ -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()
}, },

View File

@ -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,

View File

@ -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,
}, },
}) })