noot
Some checks failed
commit-tag / commit-tag-image (map[context:./migrations file:./migrations/Dockerfile name:migrations]) (push) Successful in 16s
commit-tag / commit-tag-image (map[context:./ts file:./ts/Dockerfile name:ts]) (push) Failing after 42s

This commit is contained in:
a 2025-06-14 22:32:19 -05:00
parent 97d85d7fc9
commit e55886cd8f
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
13 changed files with 164 additions and 264 deletions

View File

@ -31,7 +31,7 @@ export async function update_wynn_items() {
return
}
found_new = true
log.info(`updating wynn with new hash`, { old: currenthash, new: dataHash })
log.info('updating wynn with new hash', { old: currenthash, new: dataHash })
for (const [displayName, item] of Object.entries(parsed)) {
const json = stringify(item)
if (!json) {

View File

@ -27,7 +27,7 @@ select * from ranked
where ranked.uid = ${guild_id}
`
if (result.length == 0) {
if (result.length === 0) {
return {
content: 'No guild found.',
}
@ -36,9 +36,9 @@ where ranked.uid = ${guild_id}
const guild = result[0]
const output = [
`# 🏰 Guild Information`,
'# 🏰 Guild Information',
`## **[${guild.prefix}] ${guild.name}**\n`,
`### 📊 Statistics`,
'### 📊 Statistics',
`> **Level:** \`${guild.level}\``,
`> **Total XP:** \`${formatNumber(guild.xp)}\``,
`> **XP Rank:** \`#${guild.xp_rank >= 1000 ? '1000+' : guild.xp_rank}\``,
@ -81,7 +81,7 @@ export async function formGuildOnlineMessage(guild_id: string): Promise<CreateMe
.array()
.assert(result)
if (members.length == 0) {
if (members.length === 0) {
return {
content: '😴 No guild members are currently online.',
}
@ -97,7 +97,7 @@ export async function formGuildOnlineMessage(guild_id: string): Promise<CreateMe
// Group members by server
const membersByServer = members.reduce(
(acc, member) => {
if (acc[member.server] == undefined) {
if (acc[member.server] === undefined) {
acc[member.server] = []
}
acc[member.server].push(member)
@ -122,7 +122,7 @@ export async function formGuildOnlineMessage(guild_id: string): Promise<CreateMe
})
const output = [
`# 🟢 Online Guild Members`,
'# 🟢 Online Guild Members',
`**[${guildPrefix}] ${guildName}**\n`,
`📊 **Total Online:** \`${members.length}\` members across \`${sortedServers.length}\` servers\n`,
...serverSections,
@ -203,12 +203,12 @@ export async function formGuildLeaderboardMessage(guild_id: string): Promise<Cre
const avgContribution = Math.floor(totalXP / members.length)
const output = [
`# 📊 Guild XP Leaderboard`,
'# 📊 Guild XP Leaderboard',
`**[${guildPrefix}] ${guildName}**\n`,
`📈 **Total Guild XP:** \`${totalXP.toLocaleString()}\``,
`👥 **Total Members:** \`${members.length}\``,
`📊 **Average Contribution:** \`${avgContribution.toLocaleString()}\`\n`,
`### Top Contributors`,
'### Top Contributors',
'```',
leaderboardTable,
'```',

View File

@ -100,7 +100,6 @@ export const scrape_online_players = async () => {
log.warn(`failed to get uuid for ${playerName}`, {
err: e,
})
continue
}
}
}
@ -113,7 +112,6 @@ export const scrape_online_players = async () => {
log.warn(`failed to update server for ${playerName}`, {
err: e,
})
continue
}
}
})

View File

@ -4,7 +4,7 @@ import { c } from '#/di'
// di
import '#/services/temporal'
import path from 'path'
import path from 'node:path'
import { Client, ScheduleNotFoundError, type ScheduleOptions, ScheduleOverlapPolicy } from '@temporalio/client'
import { NativeConnection, Worker } from '@temporalio/worker'
import { PG } from '#/services/pg'
@ -129,7 +129,7 @@ export class WorkerCommand extends Command {
if (!config.resolve.alias) config.resolve.alias = {}
config.resolve.alias = {
'#': path.resolve(process.cwd(), 'src/'),
...config.resolve!.alias,
...config.resolve?.alias,
}
return config
},

View File

@ -111,27 +111,44 @@ const TEST_COMMANDS = [
},
] as const satisfies CreateApplicationCommand[]
// Helper function to create a mock handler structure with all required commands
function createMockHandlers() {
return {
simple: vi.fn(),
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
}
}
// Helper function to create a complete test setup
function createTestSetup() {
const handlers = createMockHandlers()
const notFoundHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
commands: TEST_COMMANDS,
handler: handlers,
notFoundHandler,
})
return {
handlers,
notFoundHandler,
handler,
}
}
describe('createCommandHandler', () => {
it('should handle a simple command with required string argument', async () => {
const simpleHandler = vi.fn()
const notFoundHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: simpleHandler,
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
},
notFoundHandler,
})
const { handlers, notFoundHandler, handler } = createTestSetup()
const interactionData: InteractionData = {
name: 'simple',
@ -140,31 +157,12 @@ describe('createCommandHandler', () => {
await handler(interactionData)
expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello world' })
expect(handlers.simple).toHaveBeenCalledWith({ message: 'Hello world' })
expect(notFoundHandler).not.toHaveBeenCalled()
})
it('should handle a simple command with optional arguments', async () => {
const simpleHandler = vi.fn()
const notFoundHandler = vi.fn()
let a: ExtractCommands<typeof TEST_COMMANDS>
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: simpleHandler,
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
},
notFoundHandler,
})
const { handlers, handler } = createTestSetup()
const interactionData: InteractionData = {
name: 'simple',
@ -176,30 +174,11 @@ describe('createCommandHandler', () => {
await handler(interactionData)
expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello', count: 5 })
expect(handlers.simple).toHaveBeenCalledWith({ message: 'Hello', count: 5 })
})
it('should handle subcommands correctly', async () => {
const listHandler = vi.fn()
const createHandler = vi.fn()
const notFoundHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: vi.fn(),
complex: {
list: listHandler,
create: createHandler,
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
},
notFoundHandler,
})
const { handlers, notFoundHandler, handler } = createTestSetup()
const interactionData: InteractionData = {
name: 'complex',
@ -217,30 +196,13 @@ describe('createCommandHandler', () => {
await handler(interactionData)
expect(createHandler).toHaveBeenCalledWith({ name: 'Test Item', enabled: true })
expect(listHandler).not.toHaveBeenCalled()
expect(handlers.complex.create).toHaveBeenCalledWith({ name: 'Test Item', enabled: true })
expect(handlers.complex.list).not.toHaveBeenCalled()
expect(notFoundHandler).not.toHaveBeenCalled()
})
it('should call notFoundHandler for unknown commands', async () => {
const notFoundHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: vi.fn(),
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
},
notFoundHandler,
})
const { notFoundHandler, handler } = createTestSetup()
const interactionData: InteractionData = {
name: 'unknown',
@ -253,24 +215,7 @@ describe('createCommandHandler', () => {
})
it('should call notFoundHandler for unknown subcommands', async () => {
const notFoundHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: vi.fn(),
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
},
notFoundHandler,
})
const { notFoundHandler, handler } = createTestSetup()
const interactionData: InteractionData = {
name: 'complex',
@ -289,24 +234,7 @@ describe('createCommandHandler', () => {
})
it('should handle missing interaction data', async () => {
const notFoundHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: vi.fn(),
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
},
notFoundHandler,
})
const { notFoundHandler, handler } = createTestSetup()
await handler(null as any)
expect(notFoundHandler).toHaveBeenCalledWith({})
@ -316,26 +244,7 @@ describe('createCommandHandler', () => {
})
it('should handle subcommand groups recursively', async () => {
const banHandler = vi.fn()
const kickHandler = vi.fn()
const notFoundHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: vi.fn(),
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: banHandler,
kick: kickHandler,
},
},
},
notFoundHandler,
})
const { handlers, notFoundHandler, handler } = createTestSetup()
const interactionData: InteractionData = {
name: 'admin',
@ -359,30 +268,13 @@ describe('createCommandHandler', () => {
await handler(interactionData)
expect(banHandler).toHaveBeenCalledWith({ user: '123456789', reason: 'Spam' })
expect(kickHandler).not.toHaveBeenCalled()
expect(handlers.admin.user.ban).toHaveBeenCalledWith({ user: '123456789', reason: 'Spam' })
expect(handlers.admin.user.kick).not.toHaveBeenCalled()
expect(notFoundHandler).not.toHaveBeenCalled()
})
it('should handle commands without options', async () => {
const simpleHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: simpleHandler,
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
},
notFoundHandler: vi.fn(),
})
const { handlers, handler } = createTestSetup()
const interactionData: InteractionData = {
name: 'simple',
@ -391,28 +283,11 @@ describe('createCommandHandler', () => {
await handler(interactionData)
expect(simpleHandler).toHaveBeenCalledWith({})
expect(handlers.simple).toHaveBeenCalledWith({})
})
it('should ignore subcommand options when parsing top-level args', async () => {
const simpleHandler = vi.fn()
const handler = createCommandHandler<typeof TEST_COMMANDS>({
handler: {
simple: simpleHandler,
complex: {
list: vi.fn(),
create: vi.fn(),
},
admin: {
user: {
ban: vi.fn(),
kick: vi.fn(),
},
},
},
notFoundHandler: vi.fn(),
})
const { handlers, handler } = createTestSetup()
const interactionData: InteractionData = {
name: 'simple',
@ -425,7 +300,7 @@ describe('createCommandHandler', () => {
await handler(interactionData)
expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello' })
expect(handlers.simple).toHaveBeenCalledWith({ message: 'Hello' })
})
})
@ -482,6 +357,7 @@ describe('ExtractCommands type utility', () => {
it('should infer types correctly without manual typing', () => {
// This test verifies that args are inferred correctly without manual type annotations
const handler = createCommandHandler<typeof TEST_COMMANDS>({
commands: TEST_COMMANDS,
notFoundHandler: async () => {},
handler: {
simple: async (args) => {
@ -551,7 +427,9 @@ describe('ExtractCommands type utility', () => {
expect(handlers).toBeDefined()
})
it('should handle different option types', () => {
it('should handle different option types', async () => {
const { type } = await import('arktype')
const TYPE_TEST_COMMANDS = [
{
name: 'types',
@ -573,31 +451,54 @@ describe('ExtractCommands type utility', () => {
type TypeHandlers = ExtractCommands<typeof TYPE_TEST_COMMANDS>
// Define arktype validators for each expected type
const argsValidator = type({
str: 'string',
int: 'number.integer',
bool: 'boolean',
user: 'string', // User IDs are strings
channel: 'string', // Channel IDs are strings
role: 'string', // Role IDs are strings
num: 'number',
mention: 'string', // Mentionable IDs are strings
attach: 'string', // Attachment IDs are strings
})
const handlers: TypeHandlers = {
types: async (args) => {
// Test that all types are correctly mapped
const str: string = args.str
const int: number = args.int
const bool: boolean = args.bool
const user: string = args.user
const channel: string = args.channel
const role: string = args.role
const num: number = args.num
const mention: string = args.mention
const attach: string = args.attach
// Validate the args using arktype
const result = argsValidator(args)
expect(str).toBeDefined()
expect(int).toBeDefined()
expect(bool).toBeDefined()
expect(user).toBeDefined()
expect(channel).toBeDefined()
expect(role).toBeDefined()
expect(num).toBeDefined()
expect(mention).toBeDefined()
expect(attach).toBeDefined()
// Check if validation passed (result is the validated object, not wrapped)
if (result instanceof type.errors) {
expect.fail(`Validation failed: ${result.summary}`)
} else {
// Test that all types are correctly mapped
expect(typeof result.str).toBe('string')
expect(typeof result.int).toBe('number')
expect(Number.isInteger(result.int)).toBe(true)
expect(typeof result.bool).toBe('boolean')
expect(typeof result.user).toBe('string')
expect(typeof result.channel).toBe('string')
expect(typeof result.role).toBe('string')
expect(typeof result.num).toBe('number')
expect(typeof result.mention).toBe('string')
expect(typeof result.attach).toBe('string')
}
},
}
expect(handlers).toBeDefined()
// Test with actual values
await handlers.types({
str: 'hello',
int: 42,
bool: true,
user: '123456789',
channel: '987654321',
role: '555555555',
num: 3.14,
mention: '111111111',
attach: '999999999',
})
})
})

View File

@ -1,5 +1,9 @@
import { ApplicationCommandOptionTypes, DiscordApplicationCommandOption, type CreateApplicationCommand, type DiscordInteractionDataOption } from '@discordeno/types'
import type { InteractionData } from '..'
import { ApplicationCommandOptionTypes } from '@discordeno/types'
import type {CreateApplicationCommand,
DiscordInteractionDataOption,
DiscordInteractionData,
DiscordApplicationCommandOption
}from '@discordeno/types'
import type { SLASH_COMMANDS } from './slash_commands'
// Map option types to their TypeScript types
@ -21,12 +25,12 @@ type GetOption<Options, Name> = Options extends readonly DiscordApplicationComma
// 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
@ -36,36 +40,36 @@ type HandlerFunction<Args = Record<string, never>> = (args: Args) => Promise<voi
// 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
// 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
? 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
@ -74,12 +78,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
@ -149,11 +153,11 @@ export function createCommandHandler<T extends readonly CreateApplicationCommand
handler,
notFoundHandler,
}: {
commands: T,
handler: ExtractCommands<T>
notFoundHandler: HandlerFunction<{path?: string}>
}) {
return async (data: InteractionData): Promise<void> => {
commands: T,
handler: ExtractCommands<T>
notFoundHandler: HandlerFunction<{path?: string}>
}) {
return async (data: DiscordInteractionData): Promise<void> => {
if (!data || !data.name) {
await notFoundHandler({})
return

View File

@ -2,7 +2,7 @@ import { ApplicationCommandOptionTypes, ApplicationCommandTypes, type CreateAppl
export const SLASH_COMMANDS = [
{
name: `guild`,
name: 'guild',
description: 'guild commands',
type: ApplicationCommandTypes.ChatInput,
options: [

View File

@ -1,4 +1,5 @@
import { Intents, type InteractionTypes } from '@discordeno/types'
import type { DiscordInteractionData} from '@discordeno/types'
import type { Bot, CompleteDesiredProperties, DesiredPropertiesBehavior } from 'discordeno'
export const intents = [
Intents.GuildModeration,
@ -56,11 +57,8 @@ export interface InteractionRef {
acknowledged?: boolean
}
// Type for the interaction data payload
export type InteractionData = Parameters<NonNullable<BotType['events']['interactionCreate']>>[0]['data']
// Type for the complete interaction handling payload
export interface InteractionCreatePayload {
ref: InteractionRef
data: InteractionData
data: DiscordInteractionData
}

View File

@ -1,5 +1,4 @@
import { c } from '#/di'
export class EventMux {
constructor() {}
}

View File

@ -6,10 +6,10 @@ export class TabWriter {
}
add(row: string[]) {
if (this.columns.length == 0) {
if (this.columns.length === 0) {
this.columns = new Array(row.length).fill(0).map(() => [])
}
if (row.length != this.columns.length) {
if (row.length !== this.columns.length) {
throw new Error(`Row length ${row.length} does not match columns length ${this.columns.length}`)
}
for (let i = 0; i < row.length; i++) {
@ -19,7 +19,7 @@ export class TabWriter {
build() {
let out = ''
if (this.columns.length == 0) {
if (this.columns.length === 0) {
return ''
}
const columnWidths = this.columns.map((col) => col.reduce((a, b) => Math.max(a, b.length + this.spacing), 0))

View File

@ -42,7 +42,7 @@ export class WApi {
: // When a stale state has a determined value to expire, we can use it.
// Or if the cached value cannot enter in stale state.
(value.state === 'stale' && value.ttl) || (value.state === 'cached' && !canStale(value))
? value.createdAt + value.ttl!
? value.createdAt + (value.ttl || 0)
: // otherwise, we can't determine when it should expire, so we keep
// it indefinitely.
undefined

View File

@ -12,7 +12,7 @@ export class SuperJsonPayloadConverter implements PayloadConverterWithEncoding {
public toPayload(value: unknown): Payload | undefined {
if (value === undefined) return undefined
let ejson
let ejson: any
try {
ejson = superjson.stringify(value)
} catch (e) {

View File

@ -26,13 +26,13 @@ const workflowHandleApplicationCommand = async (payload: InteractionCreatePayloa
})
}
if (!data || !data.name) {
await notFoundHandler(`Invalid command data`)
await notFoundHandler('Invalid command data')
return
}
const commandHandler = createCommandHandler({
commands: SLASH_COMMANDS,
notFoundHandler: async () => {
await notFoundHandler(`command not found`)
await notFoundHandler('command not found')
},
handler: {
player: {