diff --git a/.gitignore b/.gitignore index ff9dcbb..9ad3446 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules data db coverage +*.tsbuildinfo diff --git a/bun.lock b/bun.lock index a477d20..dfa8a52 100644 --- a/bun.lock +++ b/bun.lock @@ -23,6 +23,7 @@ "acorn-jsx": "^5.3.2", "cron-parser": "^5.3.1", "date-fns": "^4.1.0", + "domhandler": "^5.0.3", "drizzle-orm": "^0.44.5", "fp-filters": "^0.5.4", "html-dom-parser": "^5.1.1", diff --git a/packages/lib/package.json b/packages/lib/package.json index cb4fbdb..d074914 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -14,12 +14,10 @@ }, "author": "JB ", "files": [ - "dist", - "src" + "dist" ], "main": "./dist/index.js", "module": "./dist/index.js", - "types": "./dist/index*.d.ts", "exports": { ".": { "import": "./dist/index.js", @@ -62,10 +60,10 @@ "types": "./src/eve/ref/index*.d.ts", "require": "./src/eve/ref/index.js" }, - "./eve/third-party": { - "import": "./dist/eve/third-party/index.js", - "types": "./src/eve/third-party/index*.d.ts", - "require": "./src/eve/third-party/index.js" + "./eve/third-party/janice.js": { + "import": "./dist/eve/third-party/janice.js", + "types": "./dist/types/eve/third-party/janice.d.ts", + "require": "./src/eve/third-party/janice.js" }, "./eve/models": { "import": "./dist/eve/models/index.js", @@ -75,26 +73,25 @@ "./eve/data/*": "./data/*", "./discord": { "import": "./dist/discord/index.js", - "types": "./src/discord/index*.d.ts", - "require": "./src/discord/index.js" + "require": "./src/discord/index.js", + "types": "./dist/types/discord/index.d.ts" }, "./discord/commands": { - "types": "./src/discord/commands/index*.d.ts", "require": "./src/discord/commands/index.js", - "import": "./dist/discord/commands/index.js" + "import": "./dist/discord/commands/index.js", + "types": "./dist/types/discord/commands/index.d.ts" }, "./discord/components": { - "types": "./src/discord/components/index*.d.ts", + "types": "./dist/types/discord/components/index.d.ts", "require": "./src/discord/components/index.js", "import": "./dist/discord/components/index.js" }, "./discord/pages": { - "types": "./src/discord/pages/index*.d.ts", "require": "./src/discord/pages/index.js", - "import": "./dist/discord/pages/index.js" + "import": "./dist/discord/pages/index.js", + "types": "./dist/types/discord/pages/index.d.ts" }, "./discord/common": { - "types": "./src/discord/common/index*.d.ts", "require": "./src/discord/common/index.js", "import": "./dist/discord/common/index.js" }, @@ -144,23 +141,24 @@ "prettier-plugin-multiline-arrays": "^4.0.3" }, "dependencies": { + "@orama/orama": "^3.1.13", + "@oslojs/encoding": "^1.1.0", "@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev", "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "html-dom-parser": "^5.1.1", "cron-parser": "^5.3.1", "date-fns": "^4.1.0", + "domhandler": "^5.0.3", + "drizzle-orm": "^0.44.5", + "fp-filters": "^0.5.4", + "html-dom-parser": "^5.1.1", + "jsonwebtoken": "^9.0.2", + "jwk-to-pem": "^2.0.7", + "jwt-decode": "^4.0.0", "lodash": "^4.17.21", "node-cache": "^5.1.2", "stream-chain": "^3.4.0", "stream-json": "^1.9.1", - "winston": "^3.17.0", - "@orama/orama": "^3.1.13", - "@oslojs/encoding": "^1.1.0", - "drizzle-orm": "^0.44.5", - "fp-filters": "^0.5.4", - "jsonwebtoken": "^9.0.2", - "jwk-to-pem": "^2.0.7", - "jwt-decode": "^4.0.0" + "winston": "^3.17.0" } } diff --git a/packages/lib/src/discord/commands/command-context.type.ts b/packages/lib/src/discord/commands/command-context.type.ts deleted file mode 100644 index f5ffc03..0000000 --- a/packages/lib/src/discord/commands/command-context.type.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Cache } from '@/discord/core/cache.type'; -import type { KVStore } from '@/discord/core/kv-store.type.ts'; -import type { Client } from '@projectdysnomia/dysnomia'; -import type { CommandState } from './command-state'; - -export interface PartialContext { - client: Client; - cache: Cache; - kv: KVStore; - id?: string; // unique id for this command instance - state?: CommandState; // state associated with this command instance -} - -export type CommandContext = Required>; diff --git a/packages/lib/src/discord/commands/command-handler.ts b/packages/lib/src/discord/commands/command-handler.ts index 46bd8f6..faf1687 100644 --- a/packages/lib/src/discord/commands/command-handler.ts +++ b/packages/lib/src/discord/commands/command-handler.ts @@ -1,27 +1,10 @@ -import { - AutocompleteInteraction, - CommandInteraction, - ComponentInteraction, - Constants, - ModalSubmitInteraction, - type ApplicationCommandOptionAutocomplete, - type ApplicationCommandOptions, - type ApplicationCommandStructure, - type ChatInputApplicationCommandStructure, -} from '@projectdysnomia/dysnomia'; -import type { CommandContext, PartialContext } from './command-context.type'; +import { type ChatInputApplicationCommandStructure } from '@projectdysnomia/dysnomia'; +import type { ExecutableInteraction } from '../types/interaction.type'; +import type { ChatCommandDefinition, CommandContext, CommandHandler } from '../types'; -export interface CommandHandler { - definition: T; - execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise; -} - -export type ExecutableInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction | AutocompleteInteraction; - -export type ChatCommandDefinition = Omit; export function createChatCommand( definition: ChatCommandDefinition, - execute: (interaction: CommandInteraction, ctx: CommandContext) => Promise, + execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise, ): CommandHandler { const def = definition as ChatInputApplicationCommandStructure; def.type = 1; // CHAT_INPUT diff --git a/packages/lib/src/discord/commands/command-helpers.ts b/packages/lib/src/discord/commands/command-helpers.ts index bac1efb..d6fd931 100644 --- a/packages/lib/src/discord/commands/command-helpers.ts +++ b/packages/lib/src/discord/commands/command-helpers.ts @@ -1,13 +1,13 @@ -import { - Interaction, +import { Constants } from '@projectdysnomia/dysnomia'; +import type { CommandInteraction, - Constants, - ModalSubmitInteraction, - ComponentInteraction, + ExecutableInteraction, + Interaction, AutocompleteInteraction, + ComponentInteraction, + ModalSubmitInteraction, PingInteraction, -} from '@projectdysnomia/dysnomia'; -import type { ExecutableInteraction } from './command-handler'; +} from '../types'; export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction { return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND; @@ -43,3 +43,30 @@ export function getCommandName(interaction: ExecutableInteraction): string | und } return undefined; } + +export function augmentInteraction(interaction: Interaction): Interaction { + interaction.isApplicationCommand = function (): this is CommandInteraction { + return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND; + }; + interaction.isModalSubmit = function (): this is ModalSubmitInteraction { + return interaction.type === Constants.InteractionTypes.MODAL_SUBMIT; + }; + interaction.isMessageComponent = function (): this is ComponentInteraction { + return interaction.type === Constants.InteractionTypes.MESSAGE_COMPONENT; + }; + interaction.isAutocomplete = function (): this is AutocompleteInteraction { + return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE; + }; + interaction.isPing = function (): this is PingInteraction { + return interaction.type === Constants.InteractionTypes.PING; + }; + interaction.isExecutable = function (): this is ExecutableInteraction { + return ( + interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND || + interaction.type === Constants.InteractionTypes.MODAL_SUBMIT || + interaction.type === Constants.InteractionTypes.MESSAGE_COMPONENT || + interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE + ); + }; + return interaction; +} diff --git a/packages/lib/src/discord/commands/command-injection.ts b/packages/lib/src/discord/commands/command-injection.ts index d98b662..763ad28 100644 --- a/packages/lib/src/discord/commands/command-injection.ts +++ b/packages/lib/src/discord/commands/command-injection.ts @@ -1,7 +1,6 @@ import { type InteractionModalContent, type Component } from '@projectdysnomia/dysnomia'; -import type { CommandContext, PartialContext } from './command-context.type'; import { isApplicationCommand, isMessageComponent } from './command-helpers'; -import type { ExecutableInteraction } from './command-handler'; +import type { CommandContext, PartialContext, ExecutableInteraction } from '../types'; export function injectInteraction(interaction: ExecutableInteraction, ctx: PartialContext): [ExecutableInteraction, CommandContext] { // Wrap the interaction methods to inject command tracking ids into all custom_ids for modals and components. diff --git a/packages/lib/src/discord/commands/command-state.ts b/packages/lib/src/discord/commands/command-state.ts index 8e564eb..6ab056b 100644 --- a/packages/lib/src/discord/commands/command-state.ts +++ b/packages/lib/src/discord/commands/command-state.ts @@ -1,13 +1,6 @@ import { createReactiveState } from '@/util/reactive-state.js'; -import type { PartialContext } from './command-context.type'; import { isApplicationCommand, isAutocomplete } from './command-helpers'; -import type { ExecutableInteraction } from './command-handler'; - -export interface CommandState { - id: string; // unique id for this command instance - name: string; // command name - data: T; // internal data storage -} +import type { CommandState, ExecutableInteraction, PartialContext } from '../types'; export async function getCommandState(interaction: ExecutableInteraction, ctx: PartialContext): Promise> { const id = instanceIdFromInteraction(interaction); diff --git a/packages/lib/src/discord/commands/handle-commands.test.ts b/packages/lib/src/discord/commands/handle-commands.test.ts index edf83ed..2052512 100644 --- a/packages/lib/src/discord/commands/handle-commands.test.ts +++ b/packages/lib/src/discord/commands/handle-commands.test.ts @@ -1,7 +1,7 @@ import { expect, test, mock, beforeEach, afterEach } from 'bun:test'; import { handleCommands } from './handle-commands'; import { CommandInteraction, Constants, ModalSubmitInteraction, type ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; -import type { CommandHandler } from './command-handler'; +import { CommandHandler } from '../types'; let commands: Record>; diff --git a/packages/lib/src/discord/commands/handle-commands.ts b/packages/lib/src/discord/commands/handle-commands.ts index 4b5f8fc..82f3bbe 100644 --- a/packages/lib/src/discord/commands/handle-commands.ts +++ b/packages/lib/src/discord/commands/handle-commands.ts @@ -1,9 +1,9 @@ import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; -import { getCommandName, isApplicationCommand, isAutocomplete, isMessageComponent, isModalSubmit } from './command-helpers'; -import type { PartialContext } from './command-context.type'; -import type { CommandHandler, ExecutableInteraction } from './command-handler'; +import { augmentInteraction, getCommandName } from './command-helpers'; import { injectInteraction } from './command-injection'; import { getCommandState } from './command-state'; +import { type ExecutableInteraction } from '../types/interaction.type'; +import type { CommandHandler, PartialContext } from '../types'; export async function handleCommands( interaction: ExecutableInteraction, @@ -15,7 +15,7 @@ export async function handleCommands( ctx.state.name = getCommandName(interaction); } - if (isAutocomplete(interaction) && ctx.state.name) { + if (interaction.isAutocomplete() && ctx.state.name) { const acCommand = commands[ctx.state.name]; return acCommand.execute(interaction, ctx as any); } @@ -36,8 +36,9 @@ export async function handleCommands( } export function initializeCommandHandling(commands: Record>, ctx: PartialContext) { - ctx.client.on('interactionCreate', async (interaction) => { - if (isApplicationCommand(interaction) || isModalSubmit(interaction) || isMessageComponent(interaction) || isAutocomplete(interaction)) { + ctx.client.on('interactionCreate', async (_interaction) => { + const interaction = augmentInteraction(_interaction as any); + if (interaction.isExecutable()) { handleCommands(interaction, commands, ctx); } }); diff --git a/packages/lib/src/discord/commands/import-commands.ts b/packages/lib/src/discord/commands/import-commands.ts index fa55a90..4f1a191 100644 --- a/packages/lib/src/discord/commands/import-commands.ts +++ b/packages/lib/src/discord/commands/import-commands.ts @@ -1,10 +1,10 @@ import { Glob } from 'bun'; import { join } from 'node:path'; -import type { CommandHandler } from './command-handler'; import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; +import type { CommandHandler } from '../types'; export async function importCommands( - pattern: string = '**/*.command.{js,ts}', + pattern: string = '**/*.command.{js,ts,jsx,tsx}', baseDir: string = join(process.cwd(), 'src'), commandRegistry: Record> = {}, ): Promise>> { diff --git a/packages/lib/src/discord/commands/index.ts b/packages/lib/src/discord/commands/index.ts index b8f827f..dea7bd6 100644 --- a/packages/lib/src/discord/commands/index.ts +++ b/packages/lib/src/discord/commands/index.ts @@ -3,6 +3,5 @@ export * from './import-commands'; export * from './handle-commands'; export * from './command-helpers'; export * from './register-commands'; -export * from './command-context.type'; export * from './command-state'; export * from './option-builders'; diff --git a/packages/lib/src/discord/common/index.ts b/packages/lib/src/discord/common/index.ts deleted file mode 100644 index ea84ff9..0000000 --- a/packages/lib/src/discord/common/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './text'; diff --git a/packages/lib/src/discord/components/builders.ts b/packages/lib/src/discord/components/builders.ts index 281c7cc..0b32c2e 100644 --- a/packages/lib/src/discord/components/builders.ts +++ b/packages/lib/src/discord/components/builders.ts @@ -21,10 +21,12 @@ import { type URLButton, type PremiumButton, type ThumbnailComponent, + type ModalSubmitInteractionData, + type FileUploadComponent, } from '@projectdysnomia/dysnomia'; export type ActionRowItem = Button | StringSelectMenu | UserSelectMenu | RoleSelectMenu | MentionableSelectMenu | ChannelSelectMenu; -export const createActionRow = (...components: ActionRowItem[]): ActionRow => ({ +export const actionRow = (...components: ActionRowItem[]): ActionRow => ({ type: Constants.ComponentTypes.ACTION_ROW, components, }); @@ -42,7 +44,7 @@ export interface ButtonOptions { disabled?: boolean; } -export const createButton = (label: string, custom_id: string, options?: ButtonOptions): InteractionButton => ({ +export const button = (label: string, custom_id: string, options?: ButtonOptions): InteractionButton => ({ type: Constants.ComponentTypes.BUTTON, style: options?.style ?? Constants.ButtonStyles.PRIMARY, label, @@ -55,7 +57,7 @@ export interface URLButtonOptions { disabled?: boolean; } -export const createURLButton = (label: string, url: string, options?: URLButtonOptions): URLButton => ({ +export const urlButton = (label: string, url: string, options?: URLButtonOptions): URLButton => ({ type: Constants.ComponentTypes.BUTTON, style: Constants.ButtonStyles.LINK, label, @@ -68,7 +70,7 @@ export interface PremiumButtonOptions { disabled?: boolean; } -export const createPremiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({ +export const premiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({ type: Constants.ComponentTypes.BUTTON, style: Constants.ButtonStyles.PREMIUM, sku_id, @@ -87,30 +89,30 @@ export interface StringSelectOption { label: string; value: string; description?: string; - emoji?: { - name?: string; - id?: string; - animated?: boolean; - }; + emoji?: PartialEmoji; default?: boolean; } -export const createStringSelect = ( - custom_id: string, - selectOpts: StringSelectOpts, - ...options: StringSelectOption[] -): StringSelectMenu => ({ +export interface StringSelectOptions { + placeholder?: string; + min_values?: number; + max_values?: number; + disabled?: boolean; + required?: boolean; +} + +export const stringSelect = (custom_id: string, selectOpts: StringSelectOpts, ...options: StringSelectOption[]): StringSelectMenu => ({ type: Constants.ComponentTypes.STRING_SELECT, custom_id, options, - placeholder: selectOpts.placeholder ?? '', + placeholder: selectOpts.placeholder, min_values: selectOpts.min_values ?? 1, max_values: selectOpts.max_values ?? 1, disabled: selectOpts.disabled ?? false, required: selectOpts.required ?? false, // Note: not actually a property of StringSelectMenu, but useful for modals }); -export interface TextInputOptions { +export interface InputOptions { isParagraph?: boolean; label?: string; min_length?: number; @@ -120,16 +122,16 @@ export interface TextInputOptions { placeholder?: string; } -export const createTextInput = (custom_id: string, options?: TextInputOptions): TextInput => ({ +export const input = (custom_id: string, options?: InputOptions): TextInput => ({ type: Constants.ComponentTypes.TEXT_INPUT, custom_id, style: options.isParagraph ? Constants.TextInputStyles.PARAGRAPH : Constants.TextInputStyles.SHORT, - label: options?.label ?? '', + label: options?.label, min_length: options?.min_length ?? 0, max_length: options?.max_length ?? 4000, required: options?.required ?? false, - value: options?.value ?? '', - placeholder: options?.placeholder ?? '', + value: options?.value, + placeholder: options?.placeholder, }); export interface UserSelectOptions { @@ -137,10 +139,11 @@ export interface UserSelectOptions { min_values?: number; max_values?: number; disabled?: boolean; + required?: boolean; // if on a modal default_values?: Array<{ id: string; type: 'user' }>; } -export const createUserSelect = (custom_id: string, options?: UserSelectOptions): UserSelectMenu => ({ +export const userSelect = (custom_id: string, options?: UserSelectOptions): UserSelectMenu => ({ type: Constants.ComponentTypes.USER_SELECT, custom_id, placeholder: options?.placeholder ?? '', @@ -155,10 +158,11 @@ export interface RoleSelectOptions { min_values?: number; max_values?: number; disabled?: boolean; + required?: boolean; // if on a modal default_values?: Array<{ id: string; type: 'role' }>; } -export const createRoleSelect = (custom_id: string, options?: RoleSelectOptions): RoleSelectMenu => ({ +export const roleSelect = (custom_id: string, options?: RoleSelectOptions): RoleSelectMenu => ({ type: Constants.ComponentTypes.ROLE_SELECT, custom_id, placeholder: options?.placeholder ?? '', @@ -173,10 +177,11 @@ export interface MentionableSelectOptions { min_values?: number; max_values?: number; disabled?: boolean; + required?: boolean; // if on a modal default_values?: Array<{ id: string; type: 'user' | 'role' }>; } -export const createMentionableSelect = (custom_id: string, options?: MentionableSelectOptions): MentionableSelectMenu => ({ +export const mentionableSelect = (custom_id: string, options?: MentionableSelectOptions): MentionableSelectMenu => ({ type: Constants.ComponentTypes.MENTIONABLE_SELECT, custom_id, placeholder: options?.placeholder ?? '', @@ -192,10 +197,11 @@ export interface ChannelSelectOptions { min_values?: number; max_values?: number; disabled?: boolean; + required?: boolean; // if on a modal default_values?: Array<{ id: string; type: 'channel' }>; } -export const createChannelSelect = (custom_id: string, options?: ChannelSelectOptions): ChannelSelectMenu => ({ +export const channelSelect = (custom_id: string, options?: ChannelSelectOptions): ChannelSelectMenu => ({ type: Constants.ComponentTypes.CHANNEL_SELECT, custom_id, channel_types: options?.channel_types ?? [], @@ -211,7 +217,7 @@ export interface SectionOptions { accessory: Button | ThumbnailComponent; } -export const createSection = (accessory: Button | ThumbnailComponent, ...components: Array): SectionComponent => ({ +export const section = (accessory: Button | ThumbnailComponent, ...components: Array): SectionComponent => ({ type: Constants.ComponentTypes.SECTION, accessory, components, @@ -222,7 +228,7 @@ export const createSection = (accessory: Button | ThumbnailComponent, ...compone * @param content The text content to display. * @returns The created text display component. */ -export const createTextDisplay = (content: string) => ({ +export const text = (content: string) => ({ type: Constants.ComponentTypes.TEXT_DISPLAY, content, }); @@ -235,7 +241,7 @@ export interface ThumbnailOptions { spoiler?: boolean; } -export const createThumbnail = (url: string, description?: string, spoiler?: boolean): ThumbnailComponent => ({ +export const thumbnail = (url: string, description?: string, spoiler?: boolean): ThumbnailComponent => ({ type: Constants.ComponentTypes.THUMBNAIL, media: { url, @@ -250,7 +256,7 @@ export interface MediaItem { spoiler?: boolean; } -export const createMediaGallery = (...items: MediaItem[]): MediaGalleryComponent => ({ +export const gallery = (...items: MediaItem[]): MediaGalleryComponent => ({ type: Constants.ComponentTypes.MEDIA_GALLERY, items: items.map((item) => ({ type: Constants.ComponentTypes.FILE, @@ -265,7 +271,7 @@ export interface FileOptions { spoiler?: boolean; } -export const createFile = (url: string, spoiler?: boolean): FileComponent => ({ +export const file = (url: string, spoiler?: boolean): FileComponent => ({ type: Constants.ComponentTypes.FILE, file: { url, @@ -282,7 +288,7 @@ export interface SeparatorOptions { divider?: boolean; spacing?: Padding; } -export const createSeparator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({ +export const separator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({ type: Constants.ComponentTypes.SEPARATOR, divider, spacing: spacing ?? Padding.SMALL, @@ -301,14 +307,27 @@ export type ContainerItems = | SeparatorComponent | FileComponent; -export const createContainer = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({ +export const container = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({ type: Constants.ComponentTypes.CONTAINER, ...options, components, }); -export const createModalLabel = (label: string, component: TextInput | StringSelectMenu): LabelComponent => ({ +// Modals + +export const label = (label: string, component: LabelComponent['component']): LabelComponent => ({ type: Constants.ComponentTypes.LABEL, label, component, }); + +export const modal = ( + options: { custom_id?: string; title?: string }, + ...components: Array +): ModalSubmitInteractionData => + ({ + type: 9 as any, // Modal type + custom_id: options.custom_id ?? '', + title: options.title ?? '', + components, + } as any); diff --git a/packages/lib/src/discord/components/helpers.ts b/packages/lib/src/discord/components/helpers.ts index ad60474..5d9e26a 100644 --- a/packages/lib/src/discord/components/helpers.ts +++ b/packages/lib/src/discord/components/helpers.ts @@ -2,7 +2,7 @@ import { Constants, type ComponentBase, type ModalSubmitInteractionDataLabelComponent, - type ModalSubmitInteractionDataStringSelectComponent, + type ModalSubmitInteractionDataSelectComponent, type ModalSubmitInteractionDataTextInputComponent, } from '@projectdysnomia/dysnomia'; @@ -14,7 +14,7 @@ export function isModalTextInput(component: ComponentBase): component is ModalSu return component.type === Constants.ComponentTypes.TEXT_INPUT; } -export function isModalSelect(component: ComponentBase): component is ModalSubmitInteractionDataStringSelectComponent { +export function isModalSelect(component: ComponentBase): component is ModalSubmitInteractionDataSelectComponent { return component.type === Constants.ComponentTypes.STRING_SELECT; } diff --git a/packages/lib/src/discord/constants/index.ts b/packages/lib/src/discord/constants/index.ts new file mode 100644 index 0000000..e69f6f4 --- /dev/null +++ b/packages/lib/src/discord/constants/index.ts @@ -0,0 +1,2 @@ +export * from './text'; +export * from './locale'; diff --git a/packages/lib/src/discord/constants/locale.ts b/packages/lib/src/discord/constants/locale.ts new file mode 100644 index 0000000..399e45b --- /dev/null +++ b/packages/lib/src/discord/constants/locale.ts @@ -0,0 +1,34 @@ +export enum Locale { + EN_US = 'en-US', + EN_GB = 'en-GB', + DE = 'de', + ES_ES = 'es-ES', + FR = 'fr', + JA = 'ja', + KO = 'ko', + RU = 'ru', + ZH_CN = 'zh-CN', + ID = 'id', + DA = 'da', + ES_419 = 'es-419', + HR = 'hr', + IT = 'it', + LT = 'lt', + HU = 'hu', + NL = 'nl', + NO = 'no', + PL = 'pl', + PT_BR = 'pt-BR', + RO = 'ro', + FI = 'fi', + SV_SE = 'sv-SE', + VI = 'vi', + TR = 'tr', + CS = 'cs', + EL = 'el', + BG = 'bg', + UK = 'uk', + HI = 'hi', + TH = 'th', + ZH_TW = 'zh-TW', +} diff --git a/packages/lib/src/discord/common/text.ts b/packages/lib/src/discord/constants/text.ts similarity index 100% rename from packages/lib/src/discord/common/text.ts rename to packages/lib/src/discord/constants/text.ts diff --git a/packages/lib/src/discord/core/bot.ts b/packages/lib/src/discord/core/bot.ts index 82ad69f..a675a6e 100644 --- a/packages/lib/src/discord/core/bot.ts +++ b/packages/lib/src/discord/core/bot.ts @@ -15,10 +15,10 @@ export interface DiscordBotOptions { onReady?: () => void; } -export function startDiscordBot({ +export function startBot({ token = process.env.DISCORD_BOT_TOKEN || '', intents = [], - commandPattern = '**/*.command.{js,ts}', + commandPattern = '**/*.command.{js,ts,jsx,tsx}', commandBaseDir = 'src', keyStore = asyncKV, cache = kv, diff --git a/packages/lib/src/discord/index.ts b/packages/lib/src/discord/index.ts index 5c7542c..54fe764 100644 --- a/packages/lib/src/discord/index.ts +++ b/packages/lib/src/discord/index.ts @@ -1,6 +1,7 @@ -export * from './locales'; +export * from './constants'; export * from './commands'; export * from './core'; export * from './jsx'; export * from './components'; export * from './pages'; +export * from './types'; diff --git a/packages/lib/src/discord/jsd/parser.ts b/packages/lib/src/discord/jsd/parser.ts index c10a1a4..c703fb1 100644 --- a/packages/lib/src/discord/jsd/parser.ts +++ b/packages/lib/src/discord/jsd/parser.ts @@ -57,7 +57,7 @@ const JSD_INTERPOLATION = /\{(.+)\}/gs; const JSD_START_EXP_INTERPOLATION = /\{(.+)\(/gs; const JSD_END_EXP_INTERPOLATION = /\)(.+)\}/gs; -function parseText(text: string, state: state = {}, parent: Text = {}): string { +function parseText(text: string, state: state = {}, parent: Text = {} as any): string { let interpolations = text.match(JSD_INTERPOLATION); if (!interpolations) { if (text.match(JSD_START_EXP_INTERPOLATION)) { diff --git a/packages/lib/src/discord/jsx/components/action-row.ts b/packages/lib/src/discord/jsx/components/action-row.ts index a906d0e..c6d6073 100644 --- a/packages/lib/src/discord/jsx/components/action-row.ts +++ b/packages/lib/src/discord/jsx/components/action-row.ts @@ -1,5 +1,6 @@ -import { createActionRow } from '@/discord/components'; +import { actionRow } from '@/discord/components'; +import type { ActionRowElement } from './element.types'; -export function ActionRow(props: { children: any | any[] }) { - return createActionRow(...(Array.isArray(props.children) ? props.children : [props.children])); +export function ActionRow(props: { children: ActionRowElement['children'] }) { + return actionRow(...(Array.isArray(props.children) ? props.children : [props.children])); } diff --git a/packages/lib/src/discord/jsx/components/button.ts b/packages/lib/src/discord/jsx/components/button.ts index 2bc5de8..6db53ea 100644 --- a/packages/lib/src/discord/jsx/components/button.ts +++ b/packages/lib/src/discord/jsx/components/button.ts @@ -1,6 +1,14 @@ -import { createButton, type ButtonStyle } from '@/discord/components'; -import type { PartialEmoji } from '@projectdysnomia/dysnomia'; +import { button, premiumButton, urlButton } from '@/discord/components'; +import type { ButtonElement, PremiumButtonElement, URLButtonElement } from './element.types'; -export function Button(props: { label: string; customId: string; style: ButtonStyle; emoji?: PartialEmoji; disabled?: boolean }) { - return createButton(props.label, props.customId, { style: props.style, emoji: props.emoji, disabled: props.disabled }); +export function Button(props: ButtonElement['props']) { + return button(props.label, props.customId, { style: props.style, emoji: props.emoji, disabled: props.disabled }); +} + +export function URLButton(props: URLButtonElement['props']) { + return urlButton(props.label, props.url, { emoji: props.emoji, disabled: props.disabled }); +} + +export function PremiumButton(props: PremiumButtonElement['props']) { + return premiumButton(props.skuId, { emoji: props.emoji, disabled: props.disabled }); } diff --git a/packages/lib/src/discord/jsx/components/channel-select.ts b/packages/lib/src/discord/jsx/components/channel-select.ts new file mode 100644 index 0000000..7e89ea5 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/channel-select.ts @@ -0,0 +1,14 @@ +import { channelSelect } from '@/discord/components'; +import type { ChannelSelectElement } from './element.types'; + +export function ChannelSelect(props: ChannelSelectElement['props']) { + return channelSelect(props.customId, { + channel_types: props.channelTypes, + placeholder: props.placeholder, + min_values: props.minValues, + max_values: props.maxValues, + disabled: props.disabled, + required: props.required, + default_values: props.defaultValues, + }); +} diff --git a/packages/lib/src/discord/jsx/components/container.ts b/packages/lib/src/discord/jsx/components/container.ts index a504b19..cc4da48 100644 --- a/packages/lib/src/discord/jsx/components/container.ts +++ b/packages/lib/src/discord/jsx/components/container.ts @@ -1,7 +1,8 @@ -import { createContainer } from '@/discord/components'; +import { container } from '@/discord/components'; +import type { ContainerElement } from './element.types'; -export function Container(props: { accent?: number; spoiler?: boolean; children: any | any[] }) { - return createContainer( +export function Container(props: ContainerElement['props'] & { children: ContainerElement['children'] }) { + return container( { accent_color: props.accent, spoiler: props.spoiler }, ...(Array.isArray(props.children) ? props.children : [props.children]), ); diff --git a/packages/lib/src/discord/jsx/components/element.types.ts b/packages/lib/src/discord/jsx/components/element.types.ts new file mode 100644 index 0000000..78e36a3 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/element.types.ts @@ -0,0 +1,234 @@ +import type { ActionRowItem, ContainerItems, MediaItem, Padding } from '@/discord/components'; +import type { + ActionRow, + Button, + FileUploadComponent, + GuildChannelTypes, + LabelComponent, + PartialEmoji, + SelectMenu, + TextDisplayComponent, + TextInput, + ThumbnailComponent, +} from '@projectdysnomia/dysnomia'; + +export interface OptionElement { + type: 'option'; + props: { + label: string; + value: string; + description?: string; + emoji?: PartialEmoji; + default?: boolean; + }; + children: never; +} + +export interface StringSelectElement { + type: 'stringSelect'; + props: { + customId: string; + placeholder?: string; + minValues?: number; + maxValues?: number; + disabled?: boolean; + required?: boolean; // if on a modal + }; + children: OptionElement['props'] | OptionElement['props'][]; +} + +export interface LabelElement { + type: 'label'; + props: { + label: string; + }; + children: SelectMenu | TextInput | FileUploadComponent; +} + +export type ModalChildren = ActionRow | LabelComponent | TextDisplayComponent; + +export interface ModalElement { + type: 'modal'; + props: { + customId?: string; + title?: string; + }; + children: ModalChildren | ModalChildren[]; +} + +export interface ActionRowElement { + type: 'actionRow'; + props: {}; + children: ActionRowItem | ActionRowItem[]; +} + +export interface ButtonElement { + type: 'button'; + props: { + label: string; + customId: string; + style: number; + emoji?: PartialEmoji; + disabled?: boolean; + }; + children: never; +} + +export interface URLButtonElement { + type: 'urlButton'; + props: { + label: string; + url: string; + emoji?: PartialEmoji; + disabled?: boolean; + }; + children: never; +} + +export interface PremiumButtonElement { + type: 'premiumButton'; + props: { + skuId: string; + emoji?: PartialEmoji; + disabled?: boolean; + }; + children: never; +} + +export interface TextInputElement { + type: 'textInput'; + props: { + customId: string; + label?: string; // can not be set within a label on a modal + isParagraph?: boolean; + minLength?: number; + maxLength?: number; + required?: boolean; + value?: string; + placeholder?: string; + }; + children: never; +} + +export interface TextDisplayElement { + type: 'textDisplay'; + props: {}; + children: string | string[]; +} + +export interface ContainerElement { + type: 'container'; + props: { + color?: string; + accent?: number; + spoiler?: boolean; + }; + children: ContainerItems | ContainerItems[]; +} + +export interface UserSelectElement { + type: 'userSelect'; + props: { + customId: string; + placeholder?: string; + minValues?: number; + maxValues?: number; + disabled?: boolean; + required?: boolean; // if on a modal + defaultValues?: { id: string; type: 'user' }[]; + }; + children: never; +} + +export interface RoleSelectElement { + type: 'roleSelect'; + props: { + customId: string; + placeholder?: string; + minValues?: number; + maxValues?: number; + disabled?: boolean; + required?: boolean; // if on a modal + defaultValues?: { id: string; type: 'role' }[]; + }; + children: never; +} + +export interface MentionableSelectElement { + type: 'mentionableSelect'; + props: { + customId: string; + placeholder?: string; + minValues?: number; + maxValues?: number; + disabled?: boolean; + required?: boolean; // if on a modal + defaultValues?: { id: string; type: 'user' | 'role' }[]; + }; + children: never; +} + +export interface ChannelSelectElement { + type: 'channelSelect'; + props: { + customId: string; + placeholder?: string; + minValues?: number; + maxValues?: number; + disabled?: boolean; + required?: boolean; // if on a modal + channelTypes?: GuildChannelTypes[]; + defaultValues?: { id: string; type: 'channel' }[]; + }; + children: never; +} + +export interface SectionElement { + type: 'section'; + props: {}; + children: (Button | ThumbnailComponent) | [Button | ThumbnailComponent, ...Array]; +} + +export interface ThumbnailElement { + type: 'thumbnail'; + props: { + url: string; + description?: string; + spoiler?: boolean; + }; + children: never; +} + +export interface GalleryElement { + type: 'gallery'; + props: {}; + children: MediaElement['props'] | MediaElement['props'][]; +} + +export interface MediaElement { + type: 'media'; + props: { + url: string; + description?: string; + spoiler?: boolean; + }; + children: never; +} + +export interface FileElement { + type: 'file'; + props: { + url: string; + spoiler?: boolean; + }; + children: never; +} + +export interface SeparatorElement { + type: 'separator'; + props: { + divider?: boolean; + spacing?: Padding; + }; + children: never; +} diff --git a/packages/lib/src/discord/jsx/components/file.ts b/packages/lib/src/discord/jsx/components/file.ts new file mode 100644 index 0000000..9475518 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/file.ts @@ -0,0 +1,6 @@ +import { file } from '@/discord/components'; +import type { FileElement } from './element.types'; + +export function File(props: FileElement['props']) { + return file(props.url, props.spoiler); +} diff --git a/packages/lib/src/discord/jsx/components/gallery.ts b/packages/lib/src/discord/jsx/components/gallery.ts new file mode 100644 index 0000000..b4e9629 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/gallery.ts @@ -0,0 +1,7 @@ +import { gallery } from '@/discord/components'; +import type { GalleryElement } from './element.types'; + +export function Gallery(props: GalleryElement['props'] & { children: GalleryElement['children'] }) { + const children = Array.isArray(props.children) ? props.children : [props.children]; + return gallery(...children); +} diff --git a/packages/lib/src/discord/jsx/components/index.ts b/packages/lib/src/discord/jsx/components/index.ts index ee42d08..027803c 100644 --- a/packages/lib/src/discord/jsx/components/index.ts +++ b/packages/lib/src/discord/jsx/components/index.ts @@ -1,4 +1,19 @@ export * from './action-row'; export * from './button'; +export * from './channel-select'; export * from './container'; +export * from './file'; +export * from './gallery'; +export * from './label'; +export * from './media'; +export * from './mentionable-select'; +export * from './modal'; +export * from './option'; +export * from './role-select'; +export * from './section'; +export * from './separator'; +export * from './string-select'; export * from './text-display'; +export * from './text-input'; +export * from './thumbnail'; +export * from './user-select'; diff --git a/packages/lib/src/discord/jsx/components/label.ts b/packages/lib/src/discord/jsx/components/label.ts new file mode 100644 index 0000000..412c251 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/label.ts @@ -0,0 +1,6 @@ +import { label } from '@/discord/components'; +import type { LabelElement } from './element.types'; + +export function Label(props: LabelElement['props'] & { children: LabelElement['children'] }) { + return label(props.label, props.children); +} diff --git a/packages/lib/src/discord/jsx/components/media.ts b/packages/lib/src/discord/jsx/components/media.ts new file mode 100644 index 0000000..57357b7 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/media.ts @@ -0,0 +1,5 @@ +import type { MediaElement } from './element.types'; + +export function Media(props: MediaElement['props']) { + return props; +} diff --git a/packages/lib/src/discord/jsx/components/mentionable-select.ts b/packages/lib/src/discord/jsx/components/mentionable-select.ts new file mode 100644 index 0000000..13385b7 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/mentionable-select.ts @@ -0,0 +1,13 @@ +import { mentionableSelect } from '@/discord/components'; +import type { MentionableSelectElement } from './element.types'; + +export function MentionableSelect(props: MentionableSelectElement['props']) { + return mentionableSelect(props.customId, { + placeholder: props.placeholder, + min_values: props.minValues, + max_values: props.maxValues, + disabled: props.disabled, + required: props.required, + default_values: props.defaultValues, + }); +} diff --git a/packages/lib/src/discord/jsx/components/modal.ts b/packages/lib/src/discord/jsx/components/modal.ts new file mode 100644 index 0000000..d1aebd8 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/modal.ts @@ -0,0 +1,6 @@ +import { modal } from '@/discord/components'; +import type { ModalElement } from './element.types'; + +export function Modal(props: ModalElement['props'] & { children: ModalElement['children'] }) { + return modal({ custom_id: props.customId, title: props.title }, ...(Array.isArray(props.children) ? props.children : [props.children])); +} diff --git a/packages/lib/src/discord/jsx/components/option.ts b/packages/lib/src/discord/jsx/components/option.ts new file mode 100644 index 0000000..6a9dcbf --- /dev/null +++ b/packages/lib/src/discord/jsx/components/option.ts @@ -0,0 +1,5 @@ +import type { OptionElement } from './element.types'; + +export function Option(props: OptionElement['props']) { + return props; +} diff --git a/packages/lib/src/discord/jsx/components/role-select.ts b/packages/lib/src/discord/jsx/components/role-select.ts new file mode 100644 index 0000000..673c461 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/role-select.ts @@ -0,0 +1,13 @@ +import { roleSelect } from '@/discord/components'; +import type { RoleSelectElement } from './element.types'; + +export function RoleSelect(props: RoleSelectElement['props']) { + return roleSelect(props.customId, { + placeholder: props.placeholder, + min_values: props.minValues, + max_values: props.maxValues, + disabled: props.disabled, + required: props.required, + default_values: props.defaultValues, + }); +} diff --git a/packages/lib/src/discord/jsx/components/section.ts b/packages/lib/src/discord/jsx/components/section.ts new file mode 100644 index 0000000..f817d61 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/section.ts @@ -0,0 +1,7 @@ +import { section } from '@/discord/components'; +import type { SectionElement } from './element.types'; + +export function Section(props: SectionElement['props'] & { children: SectionElement['children'] }) { + const children = Array.isArray(props.children) ? props.children : [props.children]; + return section(children[0], ...(children.slice(1) as any[])); +} diff --git a/packages/lib/src/discord/jsx/components/separator.ts b/packages/lib/src/discord/jsx/components/separator.ts new file mode 100644 index 0000000..5827f04 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/separator.ts @@ -0,0 +1,6 @@ +import { separator } from '@/discord/components'; +import type { SeparatorElement } from './element.types'; + +export function Separator(props: SeparatorElement['props']) { + return separator(props.spacing, props.divider); +} diff --git a/packages/lib/src/discord/jsx/components/string-select.ts b/packages/lib/src/discord/jsx/components/string-select.ts new file mode 100644 index 0000000..eb47b49 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/string-select.ts @@ -0,0 +1,14 @@ +import { stringSelect } from '@/discord/components'; +import type { StringSelectElement } from './element.types'; + +export function StringSelect(props: StringSelectElement['props'] & { children: StringSelectElement['children'] }) { + return stringSelect( + props.customId, + { + placeholder: props.placeholder, + min_values: props.minValues, + max_values: props.maxValues, + }, + ...(Array.isArray(props.children) ? props.children : [props.children]), + ); +} diff --git a/packages/lib/src/discord/jsx/components/text-display.ts b/packages/lib/src/discord/jsx/components/text-display.ts index 16c8cd6..560f2f8 100644 --- a/packages/lib/src/discord/jsx/components/text-display.ts +++ b/packages/lib/src/discord/jsx/components/text-display.ts @@ -1,5 +1,7 @@ -import { createTextDisplay } from '@/discord/components/builders'; +import { text } from '@/discord/components/builders'; +import type { TextDisplayElement } from './element.types'; -export function TextDisplay(props: { content: string }) { - return createTextDisplay(props.content); +export function TextDisplay(props: TextDisplayElement['props'] & { children: TextDisplayElement['children'] }) { + const children = Array.isArray(props.children) ? props.children.join('') : props.children; + return text(children); } diff --git a/packages/lib/src/discord/jsx/components/text-input.ts b/packages/lib/src/discord/jsx/components/text-input.ts new file mode 100644 index 0000000..6a278c1 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/text-input.ts @@ -0,0 +1,14 @@ +import { input } from '@/discord/components'; +import type { TextInputElement } from './element.types'; + +export function TextInput(props: TextInputElement['props']) { + return input(props.customId, { + isParagraph: props.isParagraph, + label: props.label, + min_length: props.minLength, + max_length: props.maxLength, + required: props.required, + value: props.value, + placeholder: props.placeholder, + }); +} diff --git a/packages/lib/src/discord/jsx/components/thumbnail.ts b/packages/lib/src/discord/jsx/components/thumbnail.ts new file mode 100644 index 0000000..e1918c0 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/thumbnail.ts @@ -0,0 +1,6 @@ +import { thumbnail } from '@/discord/components'; +import type { ThumbnailElement } from './element.types'; + +export function Thumbnail(props: ThumbnailElement['props']) { + return thumbnail(props.url, props.description, props.spoiler); +} diff --git a/packages/lib/src/discord/jsx/components/user-select.ts b/packages/lib/src/discord/jsx/components/user-select.ts new file mode 100644 index 0000000..2f7e850 --- /dev/null +++ b/packages/lib/src/discord/jsx/components/user-select.ts @@ -0,0 +1,13 @@ +import { userSelect } from '@/discord/components'; +import type { UserSelectElement } from './element.types'; + +export function UserSelect(props: UserSelectElement['props']) { + return userSelect(props.customId, { + placeholder: props.placeholder, + min_values: props.minValues, + max_values: props.maxValues, + disabled: props.disabled, + required: props.required, + default_values: props.defaultValues, + }); +} diff --git a/packages/lib/src/discord/jsx/jsx-dev-runtime.ts b/packages/lib/src/discord/jsx/jsx-dev-runtime.ts index 5c95f0d..70763ff 100644 --- a/packages/lib/src/discord/jsx/jsx-dev-runtime.ts +++ b/packages/lib/src/discord/jsx/jsx-dev-runtime.ts @@ -1 +1,2 @@ export { jsxDEV } from './runtime'; +export type { JSX } from './jsx'; diff --git a/packages/lib/src/discord/jsx/jsx-runtime.ts b/packages/lib/src/discord/jsx/jsx-runtime.ts index 50592a8..b2e7fbc 100644 --- a/packages/lib/src/discord/jsx/jsx-runtime.ts +++ b/packages/lib/src/discord/jsx/jsx-runtime.ts @@ -1 +1,2 @@ export { jsx } from './runtime'; +export type { JSX } from './jsx'; diff --git a/packages/lib/src/discord/jsx/jsx.ts b/packages/lib/src/discord/jsx/jsx.ts index d31b5d1..e87d63e 100644 --- a/packages/lib/src/discord/jsx/jsx.ts +++ b/packages/lib/src/discord/jsx/jsx.ts @@ -3,7 +3,6 @@ import { type Button, type ChannelSelectMenu, type MentionableSelectMenu, - type PartialEmoji, type RoleSelectMenu, type StringSelectMenu, type TextInput, @@ -19,7 +18,30 @@ import { type URLButton, type PremiumButton, type ThumbnailComponent, + type ModalSubmitInteractionData, } from '@projectdysnomia/dysnomia'; +import type { + ButtonElement, + ChannelSelectElement, + ContainerElement, + FileElement, + GalleryElement, + LabelElement, + MediaElement, + MentionableSelectElement, + ModalElement, + OptionElement, + PremiumButtonElement, + RoleSelectElement, + SectionElement, + SeparatorElement, + StringSelectElement, + TextDisplayElement, + TextInputElement, + ThumbnailElement, + URLButtonElement, + UserSelectElement, +} from './components/element.types'; export type Component = | ActionRow @@ -40,30 +62,51 @@ export type Component = | InteractionButton | URLButton | PremiumButton - | ThumbnailComponent; + | ThumbnailComponent + | ModalSubmitInteractionData; -export type Element = Component | Promise; +export type StarKittenElement = Component | Promise; -export interface ElementClass { +export interface StarKittenElementClass { render: any; } -export interface ElementAttributesProperty { +export interface StarKittenElementAttributesProperty { props: {}; } -export interface IntrinsicElements { - // Allow any element, but prefer known elements - // [elemName: string]: any; - // Known elements (forcing re-parse) - actionRow: { children: any | any[] }; - button: { - label: string; - customId: string; - style: number; - emoji?: PartialEmoji; - disabled?: boolean; - }; - container: { color?: string; accent?: number; spoiler?: boolean; children: any | any[] }; - textDisplay: { content: string }; +export interface StarKittenElementChildrenAttribute { + children: {}; +} + +export interface StarKittenIntrinsicElements { + actionRow: { children: StarKittenElement | StarKittenElement[] }; + button: ButtonElement['props']; + urlButton: URLButtonElement['props']; + premiumButton: PremiumButtonElement['props']; + modal: ModalElement['props'] & { children: StarKittenElement | StarKittenElement[] }; + label: LabelElement['props'] & { children: StarKittenElement | StarKittenElement[] }; + stringSelect: StringSelectElement['props'] & { children: StringSelectElement['children'] }; + option: OptionElement['props']; + textInput: TextInputElement['props']; + textDisplay: TextDisplayElement['props']; + container: ContainerElement['props'] & { children: StarKittenElement | StarKittenElement[] }; + userSelect: UserSelectElement['props']; + roleSelect: RoleSelectElement['props']; + mentionableSelect: MentionableSelectElement['props']; + channelSelect: ChannelSelectElement['props']; + section: SectionElement['props'] & { children: StarKittenElement | StarKittenElement[] }; + thumbnail: ThumbnailElement['props']; + gallery: GalleryElement['props'] & { children: StarKittenElement | StarKittenElement[] }; + media: MediaElement['props']; + file: FileElement['props']; + separator: SeparatorElement['props']; +} + +export declare namespace JSX { + export type Element = StarKittenElement; + export interface ElementClass extends StarKittenElementClass {} + export interface ElementAttributesProperty extends StarKittenElementAttributesProperty {} + export interface ElementChildrenAttribute extends StarKittenElementChildrenAttribute {} + export interface IntrinsicElements extends StarKittenIntrinsicElements {} } diff --git a/packages/lib/src/discord/jsx/runtime.ts b/packages/lib/src/discord/jsx/runtime.ts index 08f5e61..7055996 100644 --- a/packages/lib/src/discord/jsx/runtime.ts +++ b/packages/lib/src/discord/jsx/runtime.ts @@ -1,17 +1,30 @@ -import { ActionRow } from './components/action-row'; -import { Button } from './components/button'; -import { Container } from './components/container'; -import { TextDisplay } from './components/text-display'; +import * as components from './components'; const intrinsicComponentMap: Record any> = { - actionRow: ActionRow, - button: Button, - container: Container, - textDisplay: TextDisplay, + actionRow: components.ActionRow, + button: components.Button, + container: components.Container, + file: components.File, + gallery: components.Gallery, + label: components.Label, + media: components.Media, + mentionableSelect: components.MentionableSelect, + modal: components.Modal, + option: components.Option, + premiumButton: components.PremiumButton, + roleSelect: components.RoleSelect, + section: components.Section, + separator: components.Separator, + stringSelect: components.StringSelect, + textDisplay: components.TextDisplay, + textInput: components.TextInput, + thumbnail: components.Thumbnail, + urlButton: components.URLButton, + userSelect: components.UserSelect, }; export function jsx(type: any, props: Record) { - console.log('JSX', type, props); + // console.log('JSX', type, props); if (typeof type === 'function') { return type(props); } @@ -34,7 +47,7 @@ export function jsxDEV( source: any, self: any, ) { - console.log('JSX DEV', type, props); + // console.log('JSX DEV', type, props); if (typeof type === 'function') { return type(props); } diff --git a/packages/lib/src/discord/jsx/types.d.ts b/packages/lib/src/discord/jsx/types.d.ts index 0dcdcb4..7a5f117 100644 --- a/packages/lib/src/discord/jsx/types.d.ts +++ b/packages/lib/src/discord/jsx/types.d.ts @@ -1,8 +1,22 @@ -import type { Component, IntrinsicElements as StarKittenIntrinsicElements } from './jsx'; +import type { Component, StarKittenIntrinsicElements } from './jsx'; +import type { LabelElement } from './components/label'; +import type { StringSelectElement } from './components/string-select'; +import type { PartialEmoji, StringSelectMenu, TextInput } from '@projectdysnomia/dysnomia'; -declare global { - namespace JSX { - type Element = Component; - interface IntrinsicElements extends StarKittenIntrinsicElements {} +export declare namespace JSX { + // type Element = Component; + + interface ElementClass { + render: any; } + + interface ElementAttributesProperty { + props: {}; + } + + interface ElementChildrenAttribute { + children: {}; + } + + interface IntrinsicElements extends StarKittenIntrinsicElements {} } diff --git a/packages/lib/src/discord/pages/pages.ts b/packages/lib/src/discord/pages/pages.ts index 4de441b..9a96766 100644 --- a/packages/lib/src/discord/pages/pages.ts +++ b/packages/lib/src/discord/pages/pages.ts @@ -58,7 +58,7 @@ function createPageContext(interaction: PagesInteraction, options: PagesOptio custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : options.initialPage ?? 'root', goToPage: (pageKey: string) => { const page = options.pages[pageKey]; - this.state.currentPage = pageKey; + state.currentPage = pageKey; if (!page) { throw new Error(`Page with key "${pageKey}" not found`); } diff --git a/packages/lib/src/discord/pages/subroutes.ts b/packages/lib/src/discord/pages/subroutes.ts index 8758270..9f7330d 100644 --- a/packages/lib/src/discord/pages/subroutes.ts +++ b/packages/lib/src/discord/pages/subroutes.ts @@ -1,5 +1,5 @@ import type { PartialEmoji } from '@projectdysnomia/dysnomia'; -import { createActionRow, createButton, createMediaGallery, type ButtonOptions, type ContainerItems } from '@/discord/components'; +import { actionRow, button, gallery, type ButtonOptions, type ContainerItems } from '@/discord/components'; import type { PageContext } from './pages'; export function getSubrouteKey(prefix: string, subroutes: string[]) { @@ -34,7 +34,7 @@ export function renderSubrouteButtons( .map(({ label, value, emoji }) => { const routes = [...subRoutes]; routes[subrouteIndex] = currentSubroute == value ? '_' : value; - return createButton(label, getSubrouteKey(prefix, routes), { + return button(label, getSubrouteKey(prefix, routes), { ...options, disabled: value === currentSubroute, emoji, @@ -76,10 +76,10 @@ export function renderSubroutes( const current = opts[sri] || opts[0]; const components = []; - const actionRow = createActionRow(...renderSubrouteButtons(current.value, currentSubroutes, index, prefix, opts, btnOptions)); + const actionRow = actionRow(...renderSubrouteButtons(current.value, currentSubroutes, index, prefix, opts, btnOptions)); if (current.banner) { - components.push(createMediaGallery({ url: current.banner })); + components.push(gallery({ url: current.banner })); } if (!current.actionRowPosition || current.actionRowPosition === 'top') { diff --git a/packages/lib/src/discord/types/command.type.ts b/packages/lib/src/discord/types/command.type.ts new file mode 100644 index 0000000..e78c562 --- /dev/null +++ b/packages/lib/src/discord/types/command.type.ts @@ -0,0 +1,28 @@ +import type { ChatInputApplicationCommandStructure, ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; +import type { ExecutableInteraction } from './interaction.type'; +import type { Cache } from '@/discord/core/cache.type'; +import type { KVStore } from '@/discord/core/kv-store.type.ts'; +import type { Client } from '@projectdysnomia/dysnomia'; + +export interface CommandState { + id: string; // unique id for this command instance + name: string; // command name + data: T; // internal data storage +} + +export interface PartialContext { + client: Client; + cache: Cache; + kv: KVStore; + id?: string; // unique id for this command instance + state?: CommandState; // state associated with this command instance +} + +export type CommandContext = Required>; + +export type ChatCommandDefinition = Omit; + +export interface CommandHandler { + definition: T; + execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise; +} diff --git a/packages/lib/src/discord/types/index.ts b/packages/lib/src/discord/types/index.ts new file mode 100644 index 0000000..4bd8b72 --- /dev/null +++ b/packages/lib/src/discord/types/index.ts @@ -0,0 +1,2 @@ +export * from './command.type'; +export * from './interaction.type'; diff --git a/packages/lib/src/discord/types/interaction.type.ts b/packages/lib/src/discord/types/interaction.type.ts new file mode 100644 index 0000000..6c889f5 --- /dev/null +++ b/packages/lib/src/discord/types/interaction.type.ts @@ -0,0 +1,20 @@ +import type Dysnomia from '@projectdysnomia/dysnomia'; + +export type Interaction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction | AutocompleteInteraction | PingInteraction; + +export type ExecutableInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction | AutocompleteInteraction; + +export interface InteractionAugments { + isApplicationCommand: () => this is Dysnomia.CommandInteraction; + isModalSubmit: () => this is Dysnomia.ModalSubmitInteraction; + isMessageComponent: () => this is Dysnomia.ComponentInteraction; + isAutocomplete: () => this is Dysnomia.AutocompleteInteraction; + isPing: () => this is Dysnomia.PingInteraction; + isExecutable: () => this is ExecutableInteraction; +} + +export type CommandInteraction = Dysnomia.CommandInteraction & InteractionAugments; +export type ModalSubmitInteraction = Dysnomia.ModalSubmitInteraction & InteractionAugments; +export type ComponentInteraction = Dysnomia.ComponentInteraction & InteractionAugments; +export type AutocompleteInteraction = Dysnomia.AutocompleteInteraction & InteractionAugments; +export type PingInteraction = Dysnomia.PingInteraction & InteractionAugments; diff --git a/packages/lib/src/eve/esi/auth.ts b/packages/lib/src/eve/esi/auth.ts index 5ac41f0..0c564ca 100644 --- a/packages/lib/src/eve/esi/auth.ts +++ b/packages/lib/src/eve/esi/auth.ts @@ -91,7 +91,7 @@ export async function refresh( 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${Buffer.from(`${options.client_id}:${options.client_secret}`).toString('base64')}`, }, - body: new URLSearchParams(params), + body: new URLSearchParams(params as Record), }); return (await response.json()) as EveTokens; } diff --git a/packages/lib/src/discord/locales.ts b/packages/lib/src/eve/locales.ts similarity index 58% rename from packages/lib/src/discord/locales.ts rename to packages/lib/src/eve/locales.ts index 5446668..e903fa5 100644 --- a/packages/lib/src/discord/locales.ts +++ b/packages/lib/src/eve/locales.ts @@ -13,14 +13,23 @@ export const LOCALE_NAMES: { [key in Locales]: string } = { }; export function toDiscordLocale(locale: Locales): string { switch (locale) { - case 'en': return 'en-US'; - case 'ru': return 'ru'; - case 'de': return 'de'; - case 'fr': return 'fr'; - case 'ja': return 'ja'; - case 'es': return 'es-ES'; - case 'zh': return 'zh-CN'; - case 'ko': return 'ko'; - default: return 'en-US'; + case 'en': + return 'en-US'; + case 'ru': + return 'ru'; + case 'de': + return 'de'; + case 'fr': + return 'fr'; + case 'ja': + return 'ja'; + case 'es': + return 'es-ES'; + case 'zh': + return 'zh-CN'; + case 'ko': + return 'ko'; + default: + return 'en-US'; } -} \ No newline at end of file +} diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 0ae0fbe..129dc73 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true, - "strict": true, + "strict": false, + "noImplicitAny": false, + "skipLibCheck": true, "jsx": "react-jsx", "jsxImportSource": "@star-kitten/lib/discord", "lib": ["ESNext", "DOM"], @@ -12,11 +14,14 @@ "@data/*": ["./data/*"], "@types/*": ["./types/*"] }, + "emitDeclarationOnly": true, "noEmit": false, + "noEmitOnError": false, "declaration": true, - "outDir": "dist", - "allowImportingTsExtensions": false + "outDir": "dist/types", + "rootDir": "src", + "allowImportingTsExtensions": true }, "include": ["src", "types", "src/jsx/types.d.ts"], - "exclude": ["node_modules", "dist", "build", "**/*.test.ts"] + "exclude": ["node_modules", "build", "**/*.test.ts"] } diff --git a/packages/lib/tsdown.config.ts b/packages/lib/tsdown.config.ts index 0931f62..909327e 100644 --- a/packages/lib/tsdown.config.ts +++ b/packages/lib/tsdown.config.ts @@ -18,6 +18,8 @@ export default defineConfig([ ], platform: 'node', dts: true, + minify: false, + mangle: false, external: ['bun:sqlite', 'bun'], }, ]); diff --git a/packages/lib/types/index.d.ts b/packages/lib/types/index.d.ts deleted file mode 100644 index 2268c2b..0000000 --- a/packages/lib/types/index.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - type ActionRow, - type Button, - type ChannelSelectMenu, - type GuildChannelTypes, - type MentionableSelectMenu, - type PartialEmoji, - type RoleSelectMenu, - type StringSelectMenu, - type TextInput, - type UserSelectMenu, - type LabelComponent, - type ContainerComponent, - type TextDisplayComponent, - type SectionComponent, - type MediaGalleryComponent, - type SeparatorComponent, - type FileComponent, - type InteractionButton, - type URLButton, - type PremiumButton, - type ThumbnailComponent, -} from '@projectdysnomia/dysnomia'; - -declare namespace JSX { - type Component = - | Button - | StringSelectMenu - | UserSelectMenu - | RoleSelectMenu - | MentionableSelectMenu - | ChannelSelectMenu - | TextInput - | LabelComponent - | ContainerComponent - | TextDisplayComponent - | SectionComponent - | MediaGalleryComponent - | SeparatorComponent - | FileComponent - | InteractionButton - | URLButton - | PremiumButton - | ThumbnailComponent; - - type Element = Component | Promise; - - interface ElementClass { - render: any; - } - - interface ElementAttributesProperty { - props: {}; - } - - interface IntrinsicElements { - // Allow any element, but prefer known elements - [elemName: string]: any; - // Known elements - ActionRow: { children: any | any[] }; - Button: { label: string; customId: string; style?: number; emoji?: PartialEmoji; disabled?: boolean }; - Container: { accent?: number; spoiler?: boolean; children: any | any[] }; - TextDisplay: { content: string }; - } -} diff --git a/packages/lib/wiki/discord.md b/packages/lib/wiki/discord.md new file mode 100644 index 0000000..753046e --- /dev/null +++ b/packages/lib/wiki/discord.md @@ -0,0 +1,193 @@ +# Discord + +Star Kitten's discord functionality is built on top of [Project Dysnomia](https://github.com/projectdysnomia/dysnomia) to provide up to date and performance interactions with the Discord API. + +## Create a bot + +Running a bot with Star Kitten is as simple as calling startBot()! + +_src/main.ts_ + +```ts +import { startBot } from '@star-kitten/lib/discord'; +startBot({ + token: 'XXXXXX', // can be omitted if you set a DISCORD_BOT_TOKEN environment variable +}); +``` + +You can configure the bot further through environment variables: + +_I recommend using dotenvx to encrypt your .env files rather than using plaintext .env files_ + +```.env +# Required +DISCORD_BOT_TOKEN="XXXXXX" # https://discord.com/developers/applications + +# Optional +DEBUG="true" # Enable debug mode +NODE_ENV="development" +LOG_LEVEL="debug" +RESET_COMMANDS="true" # "true" or NODE_ENV === "development" will have the bot delete and register all commands on every startup, useful for development +STAR_KITTEN_KV_DB_PATH="./kv.db" # Allows the bot to remember commands between restarts for resume functionality and recovery from crashes. If not set, :memory: is used so commands created before any restart will no longer function + +# if using EVE ESI functionality +EVE_CLIENT_ID="XXXXX" +EVE_CLIENT_SECRET="XXXX" +EVE_CALLBACK_URL="http://my.callback.url" +ESI_USER_AGENT="XXXX" # provided with each ESI request so CCP knows who to contact if there is any issue + +JANICE_KEY="XXXXX" # If you have one for using janice to get pricing data or appraisals + +# AI Features +PERPLEXITY_API_KEY="XXXXX" +``` + +Although, a bot without any commands isn't very useful, so on to Commands + +## Commands + +Star Kitten will automatically register commands for you at startup. By default, any file in your `./src` directory that matches the pattern `**/*.command.{js,ts,jsx,tsx}`, like `./src/ping.command.ts` or `./src/commands/time.command.tsx`. + +### Command File + +A command file's exports must match the [CommandHandler](../src/discord/commands/command-handler.ts#14) structure. + +definition: Defines the command to Discord, providing the command name, description, parameters and more. + +execute: Function that is ran when this command is executed. The execute method will get called for every interaction associated with that command, so ensure you properly handle different interaction types, like Autocomplete vs. ApplicationCommand. + +Example: + +```tsx +import { Constants, type ChatInputApplicationCommandStructure } from '@projectdysnomia/dysnomia'; +import { type ExecutableInteraction, type CommandContext, isApplicationCommand, Locale } from '@star-kitten/lib/discord'; + +const definition: ChatInputApplicationCommandStructure = { + type: Constants.ApplicationCommandTypes.CHAT_INPUT, + name: 'time', + nameLocalizations: { + [Locale.DE]: 'zeit', + }, + description: 'Get the current EVE time', + descriptionLocalizations: { + [Locale.DE]: 'Holen Sie sich die aktuelle EVE-Zeit', + }, +}; + +const eveTimeText = { + [Locale.EN_US]: 'EVE Time', + [Locale.DE]: 'EVE-Zeit', +}; + +function renderTimeDisplay(locale: string = 'en-US') { + const now = new Date(); + const eveTime = now.toISOString().split('T')[1].split('.')[0]; + const eveDate = now.toLocaleDateString(locale, { + timeZone: 'UTC', + year: 'numeric', + month: 'long', + day: '2-digit', + weekday: 'long', + }); + return ( + + + {`### ${eveTimeText[locale] || eveTimeText[Locale.EN_US]} +${eveTime} +${eveDate}`} + + + ); +} + +async function execute(interaction: ExecutableInteraction, ctx: CommandContext) { + if (!isApplicationCommand(interaction)) return; + + interaction.createMessage({ + flags: Constants.MessageFlags.IS_COMPONENTS_V2, + components: [renderTimeDisplay(interaction.locale)], + }); +} + +export default { + definition, + execute, +}; +``` + +## Components V2 + +Star Kitten commands should utilize components v2. We also have a JSX syntax for creating commands. Look at the examples below to compare using JSX vs plain JavaScript objecst to define your components. + +### JSX Components + +```tsx +export function renderAppraisalModal(interaction: Interaction) { + return ( + + + + + ); +} +``` + +### Plain JS Object + +```ts +export function renderAppraisalModal(interaction: Interaction) { + return { + type: Constants.InteractionResponseTypes.MODAL, + custom_id: 'appraisalResult', + title: 'Appraise Items', + components: [ + { + type: Constants.ComponentTypes.LABEL, + label: 'Select a market', + component: { + type: Constants.ComponentTypes.STRING_SELECT, + custom_id: 'market', + placeholder: 'Select a market', + min_values: 1, + max_values: 1, + options: markets.map((m) => ({ + label: m.name, + value: m.id.toString(), + default: m.id === 2, // Jita + })), + }, + }, + { + type: Constants.ComponentTypes.LABEL, + label: 'Enter items to appraise', + component: { + type: Constants.ComponentTypes.TEXT_INPUT, + custom_id: 'input', + style: Constants.TextInputStyles.PARAGRAPH, + placeholder: `e.g. Tritanium 22222 +Pyerite 8000 +Mexallon 2444`, + }, + }, + ], + }; +} +``` diff --git a/packages/star-kitten-bot/README.md b/packages/star-kitten-bot/README.md index f97ddec..05c2bc8 100644 --- a/packages/star-kitten-bot/README.md +++ b/packages/star-kitten-bot/README.md @@ -1,12 +1,12 @@ # Star Kitten Discord Bot -A Discord bot for [EVE Online](https://www.eveonline.com/). +A Discord bot for [EVE Online](https://www.eveonline.com/) built with [bun](https://bun.sh/) and [@star-kitten/lib](https://git.f302.me/jb/star-kitten) -# [Click this link to use this bot!](https://discord.com/oauth2/authorize?client_id=1288711114388930601) +# [Install Star Kitten](https://discord.com/oauth2/authorize?client_id=1288711114388930601) ## Running the Bot -This bot runs on [Bun](https://bun.sh/)! To install, run one of the following commands. +This bot runs on [Bun](https://bun.sh/)! To install Bun, run one of the following commands. _Linux & MacOS_ @@ -22,44 +22,22 @@ powershell -c "irm bun.sh/install.ps1 | iex" --- -Install dependencies. +### Install bot dependencies ```bash bun install ``` -### Link the Library & download static data - -`star-kitten-lib` has not been published, so link to it locally before running this web project. +### Download static eve reference data & Hoboleaks archive from [EVE Ref](https://everef.net/) ```bash -cd star-kitten-lib -bun link -cd ../web -bun link star-kitten-lib -``` - -### Download static eve reference data & Hoboleaks archive from [EVE Ref](https://everef.net/). - -```bash -cd star-kitten-lib bun get-data ``` -### Initialize the sqlite database - -```bash -cd star-kitten-lib -bun generate-migrations -bun migrate -``` -Drizzle's migrations seems to fail on the first try sometimes, so just grab the .sql from the generation and run those against the kitten.db file to create the tables & indexes. - ### Run the bot -Run the bot locally. ```bash -bun run dev +bun dev ``` ## Environment Variables @@ -67,6 +45,9 @@ bun run dev Create a .env file in the root directory with the following values: ```yaml +# Discord - https://discord.com/developers/applications +DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN + #General BASE_URL=http://localhost:3000 DEBUG=true @@ -74,26 +55,13 @@ PORT=3000 NODE_ENV=development LOG_LEVEL=debug - # EVE - https://developers.eveonline.com/applications EVE_CLIENT_ID=YOUR_EVE_CLIENT_ID EVE_CLIENT_SECRET=YOUR_EVE_SECRET EVE_CALLBACK_URL=http://localhost:3000/auth/callback ESI_USER_AGENT=ADD_YOUR_USER_AGENT_INFO_HERE -#Discord - https://discord.com/developers/applications -DISCORD_APP_ID=YOUR_APP_ID -DISCORD_CLIENT_SECRET=YOUR_CLIENT_SECRET -DISCORD_PUBLIC_KEY=YOUR_PUBLIC_KEY -DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN - -# ID of a test server to have immediate command refreshes -DISCORD_TEST_GUILD_ID=YOUR_TEST_SERVER_ID - # For using Janice's Appraisal API JANICE_KEY=XXX -# For using Perplexities AI API -PERPLEXITY_API_KEY=XXX - ``` diff --git a/packages/star-kitten-bot/src/commands/appraise/appraise.command.ts b/packages/star-kitten-bot/src/commands/appraise/appraise.command.ts index 3fb0445..81bf284 100644 --- a/packages/star-kitten-bot/src/commands/appraise/appraise.command.ts +++ b/packages/star-kitten-bot/src/commands/appraise/appraise.command.ts @@ -47,7 +47,7 @@ async function execute(interaction: ExecutableInteraction, ctx: CommandContext) appraiseModal: { key: 'appraiseModal', type: PageType.MODAL, - render: async () => renderAppraisalModal(interaction), + render: () => renderAppraisalModal(interaction) as any, }, appraisalResult: { key: 'appraisalResult', diff --git a/packages/star-kitten-bot/src/commands/appraise/renderAppraisal.tsx b/packages/star-kitten-bot/src/commands/appraise/renderAppraisal.tsx index 1295e36..f94c059 100644 --- a/packages/star-kitten-bot/src/commands/appraise/renderAppraisal.tsx +++ b/packages/star-kitten-bot/src/commands/appraise/renderAppraisal.tsx @@ -1,6 +1,6 @@ import type { ExecutableInteraction } from '@star-kitten/lib/discord'; import * as StarKitten from '@star-kitten/lib/discord'; -import { createActionRow, createButton, createContainer, createTextDisplay } from '@star-kitten/lib/discord/components'; +import { actionRow, button, container, text } from '@star-kitten/lib/discord/components'; import type { PageContext } from '@star-kitten/lib/discord/pages'; import { type Appraisal } from '@star-kitten/lib/eve/third-party/janice.js'; import { formatNumberToShortForm } from '@star-kitten/lib/util/text.js'; diff --git a/packages/star-kitten-bot/src/commands/appraise/renderAppraisalModal.ts b/packages/star-kitten-bot/src/commands/appraise/renderAppraisalModal.ts deleted file mode 100644 index c1b23a4..0000000 --- a/packages/star-kitten-bot/src/commands/appraise/renderAppraisalModal.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Interaction } from '@projectdysnomia/dysnomia'; -import { createModalLabel, createStringSelect, createTextInput } from '@star-kitten/lib/discord/components'; -import { markets } from '@star-kitten/lib/eve/third-party/janice.js'; - -export function renderAppraisalModal(interaction: Interaction) { - return { - // next page to render will be appraisalResult - custom_id: `appraisalResult`, - title: 'Appraise Items', - components: [ - createModalLabel( - 'Select your market (default: Jita)', - createStringSelect( - 'market', - { - placeholder: 'Select a market', - }, - ...markets.map((m) => ({ - label: m.name, - value: m.id.toString(), - default: m.id === 2, // Jita - })), - ), - ), - createModalLabel( - 'Enter items to appraise', - createTextInput('input', { - isParagraph: true, - placeholder: `Enter list of items to be appraised. -Tritanium 22222 -Pyerite 8000 -Mexallon 2444`, - }), - ), - ], - }; -} diff --git a/packages/star-kitten-bot/src/commands/appraise/renderAppraisalModal.tsx b/packages/star-kitten-bot/src/commands/appraise/renderAppraisalModal.tsx new file mode 100644 index 0000000..804ec87 --- /dev/null +++ b/packages/star-kitten-bot/src/commands/appraise/renderAppraisalModal.tsx @@ -0,0 +1,29 @@ +import { markets } from '@star-kitten/lib/eve/third-party/janice.js'; +import type { ExecutableInteraction } from '@star-kitten/lib/discord'; + +export function renderAppraisalModal(interaction: ExecutableInteraction) { + return ( + + + + + ); +} diff --git a/packages/star-kitten-bot/src/commands/search/pages/attributes.ts b/packages/star-kitten-bot/src/commands/search/pages/attributes.ts index 9edaab5..1da6cc2 100644 --- a/packages/star-kitten-bot/src/commands/search/pages/attributes.ts +++ b/packages/star-kitten-bot/src/commands/search/pages/attributes.ts @@ -2,11 +2,11 @@ import { renderSubroutes, type Page } from '@star-kitten/lib/discord/pages'; import type { SearchState } from '../search.command'; import { ButtonStyle, - createContainer, - createSection, - createSeparator, - createTextDisplay, - createThumbnail, + container, + section, + separator, + text, + thumbnail, Padding, } from '@star-kitten/lib/discord/components'; import { @@ -88,11 +88,11 @@ const page: Page = { return { components: [ - createContainer( + container( {}, - createSection( - createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`), - createTextDisplay(`# [${type.name.en}](https://everef.net/types/${type.type_id})\n## Attributes`), + section( + thumbnail(`https://images.evetech.net/types/${type.type_id}/icon`), + text(`# [${type.name.en}](https://everef.net/types/${type.type_id})\n## Attributes`), ), ...renderSubroutes( context, @@ -125,11 +125,11 @@ const page: Page = { const unit = attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : ''; lines.push(`${attr.attribute.display_name.en.padEnd(24)} ${unit}`); }); - return createTextDisplay('```\n' + lines.join('\n') + '\n```'); + return text('```\n' + lines.join('\n') + '\n```'); }, { style: ButtonStyle.SECONDARY }, ), - createSeparator(Padding.LARGE), + separator(Padding.LARGE), searchActionRow('attributes'), ), ], diff --git a/packages/star-kitten-bot/src/commands/search/pages/helpers.ts b/packages/star-kitten-bot/src/commands/search/pages/helpers.ts index daaf114..dea9603 100644 --- a/packages/star-kitten-bot/src/commands/search/pages/helpers.ts +++ b/packages/star-kitten-bot/src/commands/search/pages/helpers.ts @@ -1,11 +1,11 @@ -import { createActionRow, createButton } from '@star-kitten/lib/discord/components'; +import { actionRow, button } from '@star-kitten/lib/discord/components'; export function searchActionRow(pageKey: string) { - return createActionRow( - createButton('Main', 'main', { disabled: pageKey === 'main' }), - createButton('Attributes', 'attributes', { disabled: pageKey === 'attributes' }), - createButton('Fittings', 'fittings', { disabled: pageKey === 'fittings' }), - createButton('Skills', 'skills', { disabled: pageKey === 'skills' }), - createButton('Industry', 'industry', { disabled: pageKey === 'industry' }), + return actionRow( + button('Main', 'main', { disabled: pageKey === 'main' }), + button('Attributes', 'attributes', { disabled: pageKey === 'attributes' }), + button('Fittings', 'fittings', { disabled: pageKey === 'fittings' }), + button('Skills', 'skills', { disabled: pageKey === 'skills' }), + button('Industry', 'industry', { disabled: pageKey === 'industry' }), ); } diff --git a/packages/star-kitten-bot/src/commands/search/pages/main.ts b/packages/star-kitten-bot/src/commands/search/pages/main.ts index 5d8eeb4..8e65607 100644 --- a/packages/star-kitten-bot/src/commands/search/pages/main.ts +++ b/packages/star-kitten-bot/src/commands/search/pages/main.ts @@ -1,13 +1,6 @@ import type { Page } from '@star-kitten/lib/discord/pages'; import type { SearchState } from '../search.command'; -import { - createContainer, - createMediaGallery, - createSection, - createTextDisplay, - createThumbnail, - createURLButton, -} from '@star-kitten/lib/discord/components'; +import { container, gallery, section, text, thumbnail, urlButton } from '@star-kitten/lib/discord/components'; import { getRoleBonuses, getSkillBonuses, getType } from '@star-kitten/lib/eve/models/type.js'; import { cleanText } from '@star-kitten/lib/eve/utils/markdown.js'; import { typeSearch } from '@star-kitten/lib/eve/utils/typeSearch.js'; @@ -25,7 +18,7 @@ const page: Page = { if (!found) { return { - components: [createTextDisplay(`No item found for: ${typeName}`)], + components: [text(`No item found for: ${typeName}`)], }; } @@ -40,11 +33,11 @@ const page: Page = { return { components: [ - createContainer( + container( {}, - createSection( - createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`), - createTextDisplay(` + section( + thumbnail(`https://images.evetech.net/types/${type.type_id}/icon`), + text(` # [${type.name.en}](https://everef.net/types/${type.type_id}) ${skillBonuses @@ -67,18 +60,18 @@ ${roleBonuses } `), ), - createMediaGallery({ + gallery({ url: 'https://iili.io/KTPCFRt.md.webp', }), // createSeparator(Padding.LARGE), - createSection( - createURLButton('View on EVE Tycoon', `https://evetycoon.com/market/${type.type_id}`), - createTextDisplay( + section( + urlButton('View on EVE Tycoon', `https://evetycoon.com/market/${type.type_id}`), + text( `## Buy: ${price ? formatNumberToShortForm(price.buyAvgFivePercent) : '--'} ISK ## Sell: ${price ? formatNumberToShortForm(price.sellAvgFivePercent) : '--'} ISK`, ), ), - createTextDisplay(`-# Type Id: ${type.type_id}`), + text(`-# Type Id: ${type.type_id}`), searchActionRow('main'), ), ], diff --git a/packages/star-kitten-bot/src/commands/time.command.tsx b/packages/star-kitten-bot/src/commands/time.command.tsx new file mode 100644 index 0000000..a46c9dc --- /dev/null +++ b/packages/star-kitten-bot/src/commands/time.command.tsx @@ -0,0 +1,78 @@ +import { Constants, type ChatInputApplicationCommandStructure } from '@projectdysnomia/dysnomia'; +import { + type ExecutableInteraction, + type CommandContext, + isApplicationCommand, + Locale, +} from '@star-kitten/lib/discord'; + +const definition: ChatInputApplicationCommandStructure = { + type: Constants.ApplicationCommandTypes.CHAT_INPUT, + name: 'time', + nameLocalizations: { + [Locale.DE]: 'zeit', + [Locale.ES_ES]: 'hora', + [Locale.FR]: 'heure', + [Locale.JA]: '時間', + [Locale.KO]: '시간', + [Locale.RU]: 'время', + [Locale.ZH_CN]: '时间', + }, + description: 'Get the current EVE time', + descriptionLocalizations: { + [Locale.DE]: 'Holen Sie sich die aktuelle EVE-Zeit', + [Locale.ES_ES]: 'Obtén la hora actual de EVE', + [Locale.FR]: "Obtenez l'heure actuelle d'EVE", + [Locale.JA]: '現在のEVE時間を取得します', + [Locale.KO]: '현재 EVE 시간을 가져옵니다', + [Locale.RU]: 'Получите текущее время EVE', + [Locale.ZH_CN]: '获取当前的EVE时间', + }, +}; + +const eveTimeText = { + [Locale.EN_US]: 'EVE Time', + [Locale.EN_GB]: 'EVE Time', + [Locale.DE]: 'EVE-Zeit', + [Locale.ES_ES]: 'Hora EVE', + [Locale.FR]: "Heure d'EVE", + [Locale.JA]: 'EVE時間', + [Locale.KO]: 'EVE 시간', + [Locale.RU]: 'Время EVE', + [Locale.ZH_CN]: 'EVE时间', +}; + +function renderTimeDisplay(locale: string = 'en-US') { + const now = new Date(); + const eveTime = now.toISOString().split('T')[1].split('.')[0]; + const eveDate = now.toLocaleDateString(locale, { + timeZone: 'UTC', + year: 'numeric', + month: 'long', + day: '2-digit', + weekday: 'long', + }); + return ( + + + {`### ${eveTimeText[locale] || eveTimeText[Locale.EN_US]} +${eveTime} +${eveDate}`} + + + ); +} + +async function execute(interaction: ExecutableInteraction, ctx: CommandContext) { + if (!isApplicationCommand(interaction)) return; + + interaction.createMessage({ + flags: Constants.MessageFlags.IS_COMPONENTS_V2, + components: [renderTimeDisplay(interaction.locale)], + }); +} + +export default { + definition, + execute, +}; diff --git a/packages/star-kitten-bot/src/main.ts b/packages/star-kitten-bot/src/main.ts index 66e7323..b2ecf24 100644 --- a/packages/star-kitten-bot/src/main.ts +++ b/packages/star-kitten-bot/src/main.ts @@ -1,3 +1,3 @@ -import { startDiscordBot } from '@star-kitten/lib/discord'; +import { startBot } from '@star-kitten/lib/discord'; -startDiscordBot(); +startBot(); diff --git a/packages/star-kitten-bot/src/test.tsx b/packages/star-kitten-bot/src/test.tsx deleted file mode 100644 index 56a0bae..0000000 --- a/packages/star-kitten-bot/src/test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -export function renderAppraisal() { - const formatter = new Intl.NumberFormat('en-US', { - maximumFractionDigits: 2, - minimumFractionDigits: 2, - }); - const world = 'world'; - const rand = Math.random() * 1000; - const pageCtx = { state: { currentPage: 'home' } }; - - let jsx = ( - - - - {pageCtx.state.currentPage !== 'share' ? ( - -