JSX component support and time command

This commit is contained in:
JB
2026-01-02 16:08:20 -05:00
parent eece9b1257
commit bba8c4f6f1
71 changed files with 1214 additions and 429 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ node_modules
data data
db db
coverage coverage
*.tsbuildinfo

View File

@@ -23,6 +23,7 @@
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"cron-parser": "^5.3.1", "cron-parser": "^5.3.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"domhandler": "^5.0.3",
"drizzle-orm": "^0.44.5", "drizzle-orm": "^0.44.5",
"fp-filters": "^0.5.4", "fp-filters": "^0.5.4",
"html-dom-parser": "^5.1.1", "html-dom-parser": "^5.1.1",

View File

@@ -14,12 +14,10 @@
}, },
"author": "JB <j-b-3.deviate267@passmail.net>", "author": "JB <j-b-3.deviate267@passmail.net>",
"files": [ "files": [
"dist", "dist"
"src"
], ],
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index*.d.ts",
"exports": { "exports": {
".": { ".": {
"import": "./dist/index.js", "import": "./dist/index.js",
@@ -62,10 +60,10 @@
"types": "./src/eve/ref/index*.d.ts", "types": "./src/eve/ref/index*.d.ts",
"require": "./src/eve/ref/index.js" "require": "./src/eve/ref/index.js"
}, },
"./eve/third-party": { "./eve/third-party/janice.js": {
"import": "./dist/eve/third-party/index.js", "import": "./dist/eve/third-party/janice.js",
"types": "./src/eve/third-party/index*.d.ts", "types": "./dist/types/eve/third-party/janice.d.ts",
"require": "./src/eve/third-party/index.js" "require": "./src/eve/third-party/janice.js"
}, },
"./eve/models": { "./eve/models": {
"import": "./dist/eve/models/index.js", "import": "./dist/eve/models/index.js",
@@ -75,26 +73,25 @@
"./eve/data/*": "./data/*", "./eve/data/*": "./data/*",
"./discord": { "./discord": {
"import": "./dist/discord/index.js", "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": { "./discord/commands": {
"types": "./src/discord/commands/index*.d.ts",
"require": "./src/discord/commands/index.js", "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": { "./discord/components": {
"types": "./src/discord/components/index*.d.ts", "types": "./dist/types/discord/components/index.d.ts",
"require": "./src/discord/components/index.js", "require": "./src/discord/components/index.js",
"import": "./dist/discord/components/index.js" "import": "./dist/discord/components/index.js"
}, },
"./discord/pages": { "./discord/pages": {
"types": "./src/discord/pages/index*.d.ts",
"require": "./src/discord/pages/index.js", "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": { "./discord/common": {
"types": "./src/discord/common/index*.d.ts",
"require": "./src/discord/common/index.js", "require": "./src/discord/common/index.js",
"import": "./dist/discord/common/index.js" "import": "./dist/discord/common/index.js"
}, },
@@ -144,23 +141,24 @@
"prettier-plugin-multiline-arrays": "^4.0.3" "prettier-plugin-multiline-arrays": "^4.0.3"
}, },
"dependencies": { "dependencies": {
"@orama/orama": "^3.1.13",
"@oslojs/encoding": "^1.1.0",
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev", "@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
"acorn": "^8.14.0", "acorn": "^8.14.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"html-dom-parser": "^5.1.1",
"cron-parser": "^5.3.1", "cron-parser": "^5.3.1",
"date-fns": "^4.1.0", "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", "lodash": "^4.17.21",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"stream-chain": "^3.4.0", "stream-chain": "^3.4.0",
"stream-json": "^1.9.1", "stream-json": "^1.9.1",
"winston": "^3.17.0", "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"
} }
} }

View File

@@ -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<T = any> {
client: Client;
cache: Cache;
kv: KVStore;
id?: string; // unique id for this command instance
state?: CommandState<T>; // state associated with this command instance
}
export type CommandContext<T = any> = Required<PartialContext<T>>;

View File

@@ -1,27 +1,10 @@
import { import { type ChatInputApplicationCommandStructure } from '@projectdysnomia/dysnomia';
AutocompleteInteraction, import type { ExecutableInteraction } from '../types/interaction.type';
CommandInteraction, import type { ChatCommandDefinition, CommandContext, CommandHandler } from '../types';
ComponentInteraction,
Constants,
ModalSubmitInteraction,
type ApplicationCommandOptionAutocomplete,
type ApplicationCommandOptions,
type ApplicationCommandStructure,
type ChatInputApplicationCommandStructure,
} from '@projectdysnomia/dysnomia';
import type { CommandContext, PartialContext } from './command-context.type';
export interface CommandHandler<T extends ApplicationCommandStructure> {
definition: T;
execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise<void>;
}
export type ExecutableInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction | AutocompleteInteraction;
export type ChatCommandDefinition = Omit<ChatInputApplicationCommandStructure, 'type'>;
export function createChatCommand( export function createChatCommand(
definition: ChatCommandDefinition, definition: ChatCommandDefinition,
execute: (interaction: CommandInteraction, ctx: CommandContext) => Promise<void>, execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise<void>,
): CommandHandler<ChatInputApplicationCommandStructure> { ): CommandHandler<ChatInputApplicationCommandStructure> {
const def = definition as ChatInputApplicationCommandStructure; const def = definition as ChatInputApplicationCommandStructure;
def.type = 1; // CHAT_INPUT def.type = 1; // CHAT_INPUT

View File

@@ -1,13 +1,13 @@
import { import { Constants } from '@projectdysnomia/dysnomia';
Interaction, import type {
CommandInteraction, CommandInteraction,
Constants, ExecutableInteraction,
ModalSubmitInteraction, Interaction,
ComponentInteraction,
AutocompleteInteraction, AutocompleteInteraction,
ComponentInteraction,
ModalSubmitInteraction,
PingInteraction, PingInteraction,
} from '@projectdysnomia/dysnomia'; } from '../types';
import type { ExecutableInteraction } from './command-handler';
export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction { export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction {
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND; return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND;
@@ -43,3 +43,30 @@ export function getCommandName(interaction: ExecutableInteraction): string | und
} }
return undefined; 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;
}

View File

@@ -1,7 +1,6 @@
import { type InteractionModalContent, type Component } from '@projectdysnomia/dysnomia'; import { type InteractionModalContent, type Component } from '@projectdysnomia/dysnomia';
import type { CommandContext, PartialContext } from './command-context.type';
import { isApplicationCommand, isMessageComponent } from './command-helpers'; 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] { 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. // Wrap the interaction methods to inject command tracking ids into all custom_ids for modals and components.

View File

@@ -1,13 +1,6 @@
import { createReactiveState } from '@/util/reactive-state.js'; import { createReactiveState } from '@/util/reactive-state.js';
import type { PartialContext } from './command-context.type';
import { isApplicationCommand, isAutocomplete } from './command-helpers'; import { isApplicationCommand, isAutocomplete } from './command-helpers';
import type { ExecutableInteraction } from './command-handler'; import type { CommandState, ExecutableInteraction, PartialContext } from '../types';
export interface CommandState<T = any> {
id: string; // unique id for this command instance
name: string; // command name
data: T; // internal data storage
}
export async function getCommandState<T>(interaction: ExecutableInteraction, ctx: PartialContext): Promise<CommandState<T>> { export async function getCommandState<T>(interaction: ExecutableInteraction, ctx: PartialContext): Promise<CommandState<T>> {
const id = instanceIdFromInteraction(interaction); const id = instanceIdFromInteraction(interaction);

View File

@@ -1,7 +1,7 @@
import { expect, test, mock, beforeEach, afterEach } from 'bun:test'; import { expect, test, mock, beforeEach, afterEach } from 'bun:test';
import { handleCommands } from './handle-commands'; import { handleCommands } from './handle-commands';
import { CommandInteraction, Constants, ModalSubmitInteraction, type ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; import { CommandInteraction, Constants, ModalSubmitInteraction, type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
import type { CommandHandler } from './command-handler'; import { CommandHandler } from '../types';
let commands: Record<string, CommandHandler<ApplicationCommandStructure>>; let commands: Record<string, CommandHandler<ApplicationCommandStructure>>;

View File

@@ -1,9 +1,9 @@
import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
import { getCommandName, isApplicationCommand, isAutocomplete, isMessageComponent, isModalSubmit } from './command-helpers'; import { augmentInteraction, getCommandName } from './command-helpers';
import type { PartialContext } from './command-context.type';
import type { CommandHandler, ExecutableInteraction } from './command-handler';
import { injectInteraction } from './command-injection'; import { injectInteraction } from './command-injection';
import { getCommandState } from './command-state'; import { getCommandState } from './command-state';
import { type ExecutableInteraction } from '../types/interaction.type';
import type { CommandHandler, PartialContext } from '../types';
export async function handleCommands( export async function handleCommands(
interaction: ExecutableInteraction, interaction: ExecutableInteraction,
@@ -15,7 +15,7 @@ export async function handleCommands(
ctx.state.name = getCommandName(interaction); ctx.state.name = getCommandName(interaction);
} }
if (isAutocomplete(interaction) && ctx.state.name) { if (interaction.isAutocomplete() && ctx.state.name) {
const acCommand = commands[ctx.state.name]; const acCommand = commands[ctx.state.name];
return acCommand.execute(interaction, ctx as any); return acCommand.execute(interaction, ctx as any);
} }
@@ -36,8 +36,9 @@ export async function handleCommands(
} }
export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) { export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) {
ctx.client.on('interactionCreate', async (interaction) => { ctx.client.on('interactionCreate', async (_interaction) => {
if (isApplicationCommand(interaction) || isModalSubmit(interaction) || isMessageComponent(interaction) || isAutocomplete(interaction)) { const interaction = augmentInteraction(_interaction as any);
if (interaction.isExecutable()) {
handleCommands(interaction, commands, ctx); handleCommands(interaction, commands, ctx);
} }
}); });

View File

@@ -1,10 +1,10 @@
import { Glob } from 'bun'; import { Glob } from 'bun';
import { join } from 'node:path'; import { join } from 'node:path';
import type { CommandHandler } from './command-handler';
import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia'; import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
import type { CommandHandler } from '../types';
export async function importCommands( export async function importCommands(
pattern: string = '**/*.command.{js,ts}', pattern: string = '**/*.command.{js,ts,jsx,tsx}',
baseDir: string = join(process.cwd(), 'src'), baseDir: string = join(process.cwd(), 'src'),
commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {}, commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {},
): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> { ): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> {

View File

@@ -3,6 +3,5 @@ export * from './import-commands';
export * from './handle-commands'; export * from './handle-commands';
export * from './command-helpers'; export * from './command-helpers';
export * from './register-commands'; export * from './register-commands';
export * from './command-context.type';
export * from './command-state'; export * from './command-state';
export * from './option-builders'; export * from './option-builders';

View File

@@ -1 +0,0 @@
export * from './text';

View File

@@ -21,10 +21,12 @@ import {
type URLButton, type URLButton,
type PremiumButton, type PremiumButton,
type ThumbnailComponent, type ThumbnailComponent,
type ModalSubmitInteractionData,
type FileUploadComponent,
} from '@projectdysnomia/dysnomia'; } from '@projectdysnomia/dysnomia';
export type ActionRowItem = Button | StringSelectMenu | UserSelectMenu | RoleSelectMenu | MentionableSelectMenu | ChannelSelectMenu; 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, type: Constants.ComponentTypes.ACTION_ROW,
components, components,
}); });
@@ -42,7 +44,7 @@ export interface ButtonOptions {
disabled?: boolean; 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, type: Constants.ComponentTypes.BUTTON,
style: options?.style ?? Constants.ButtonStyles.PRIMARY, style: options?.style ?? Constants.ButtonStyles.PRIMARY,
label, label,
@@ -55,7 +57,7 @@ export interface URLButtonOptions {
disabled?: boolean; 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, type: Constants.ComponentTypes.BUTTON,
style: Constants.ButtonStyles.LINK, style: Constants.ButtonStyles.LINK,
label, label,
@@ -68,7 +70,7 @@ export interface PremiumButtonOptions {
disabled?: boolean; disabled?: boolean;
} }
export const createPremiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({ export const premiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({
type: Constants.ComponentTypes.BUTTON, type: Constants.ComponentTypes.BUTTON,
style: Constants.ButtonStyles.PREMIUM, style: Constants.ButtonStyles.PREMIUM,
sku_id, sku_id,
@@ -87,30 +89,30 @@ export interface StringSelectOption {
label: string; label: string;
value: string; value: string;
description?: string; description?: string;
emoji?: { emoji?: PartialEmoji;
name?: string;
id?: string;
animated?: boolean;
};
default?: boolean; default?: boolean;
} }
export const createStringSelect = ( export interface StringSelectOptions {
custom_id: string, placeholder?: string;
selectOpts: StringSelectOpts, min_values?: number;
...options: StringSelectOption[] max_values?: number;
): StringSelectMenu => ({ disabled?: boolean;
required?: boolean;
}
export const stringSelect = (custom_id: string, selectOpts: StringSelectOpts, ...options: StringSelectOption[]): StringSelectMenu => ({
type: Constants.ComponentTypes.STRING_SELECT, type: Constants.ComponentTypes.STRING_SELECT,
custom_id, custom_id,
options, options,
placeholder: selectOpts.placeholder ?? '', placeholder: selectOpts.placeholder,
min_values: selectOpts.min_values ?? 1, min_values: selectOpts.min_values ?? 1,
max_values: selectOpts.max_values ?? 1, max_values: selectOpts.max_values ?? 1,
disabled: selectOpts.disabled ?? false, disabled: selectOpts.disabled ?? false,
required: selectOpts.required ?? false, // Note: not actually a property of StringSelectMenu, but useful for modals required: selectOpts.required ?? false, // Note: not actually a property of StringSelectMenu, but useful for modals
}); });
export interface TextInputOptions { export interface InputOptions {
isParagraph?: boolean; isParagraph?: boolean;
label?: string; label?: string;
min_length?: number; min_length?: number;
@@ -120,16 +122,16 @@ export interface TextInputOptions {
placeholder?: string; 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, type: Constants.ComponentTypes.TEXT_INPUT,
custom_id, custom_id,
style: options.isParagraph ? Constants.TextInputStyles.PARAGRAPH : Constants.TextInputStyles.SHORT, style: options.isParagraph ? Constants.TextInputStyles.PARAGRAPH : Constants.TextInputStyles.SHORT,
label: options?.label ?? '', label: options?.label,
min_length: options?.min_length ?? 0, min_length: options?.min_length ?? 0,
max_length: options?.max_length ?? 4000, max_length: options?.max_length ?? 4000,
required: options?.required ?? false, required: options?.required ?? false,
value: options?.value ?? '', value: options?.value,
placeholder: options?.placeholder ?? '', placeholder: options?.placeholder,
}); });
export interface UserSelectOptions { export interface UserSelectOptions {
@@ -137,10 +139,11 @@ export interface UserSelectOptions {
min_values?: number; min_values?: number;
max_values?: number; max_values?: number;
disabled?: boolean; disabled?: boolean;
required?: boolean; // if on a modal
default_values?: Array<{ id: string; type: 'user' }>; 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, type: Constants.ComponentTypes.USER_SELECT,
custom_id, custom_id,
placeholder: options?.placeholder ?? '', placeholder: options?.placeholder ?? '',
@@ -155,10 +158,11 @@ export interface RoleSelectOptions {
min_values?: number; min_values?: number;
max_values?: number; max_values?: number;
disabled?: boolean; disabled?: boolean;
required?: boolean; // if on a modal
default_values?: Array<{ id: string; type: 'role' }>; 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, type: Constants.ComponentTypes.ROLE_SELECT,
custom_id, custom_id,
placeholder: options?.placeholder ?? '', placeholder: options?.placeholder ?? '',
@@ -173,10 +177,11 @@ export interface MentionableSelectOptions {
min_values?: number; min_values?: number;
max_values?: number; max_values?: number;
disabled?: boolean; disabled?: boolean;
required?: boolean; // if on a modal
default_values?: Array<{ id: string; type: 'user' | 'role' }>; 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, type: Constants.ComponentTypes.MENTIONABLE_SELECT,
custom_id, custom_id,
placeholder: options?.placeholder ?? '', placeholder: options?.placeholder ?? '',
@@ -192,10 +197,11 @@ export interface ChannelSelectOptions {
min_values?: number; min_values?: number;
max_values?: number; max_values?: number;
disabled?: boolean; disabled?: boolean;
required?: boolean; // if on a modal
default_values?: Array<{ id: string; type: 'channel' }>; 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, type: Constants.ComponentTypes.CHANNEL_SELECT,
custom_id, custom_id,
channel_types: options?.channel_types ?? [], channel_types: options?.channel_types ?? [],
@@ -211,7 +217,7 @@ export interface SectionOptions {
accessory: Button | ThumbnailComponent; accessory: Button | ThumbnailComponent;
} }
export const createSection = (accessory: Button | ThumbnailComponent, ...components: Array<TextDisplayComponent>): SectionComponent => ({ export const section = (accessory: Button | ThumbnailComponent, ...components: Array<TextDisplayComponent>): SectionComponent => ({
type: Constants.ComponentTypes.SECTION, type: Constants.ComponentTypes.SECTION,
accessory, accessory,
components, components,
@@ -222,7 +228,7 @@ export const createSection = (accessory: Button | ThumbnailComponent, ...compone
* @param content The text content to display. * @param content The text content to display.
* @returns The created text display component. * @returns The created text display component.
*/ */
export const createTextDisplay = (content: string) => ({ export const text = (content: string) => ({
type: Constants.ComponentTypes.TEXT_DISPLAY, type: Constants.ComponentTypes.TEXT_DISPLAY,
content, content,
}); });
@@ -235,7 +241,7 @@ export interface ThumbnailOptions {
spoiler?: boolean; 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, type: Constants.ComponentTypes.THUMBNAIL,
media: { media: {
url, url,
@@ -250,7 +256,7 @@ export interface MediaItem {
spoiler?: boolean; spoiler?: boolean;
} }
export const createMediaGallery = (...items: MediaItem[]): MediaGalleryComponent => ({ export const gallery = (...items: MediaItem[]): MediaGalleryComponent => ({
type: Constants.ComponentTypes.MEDIA_GALLERY, type: Constants.ComponentTypes.MEDIA_GALLERY,
items: items.map((item) => ({ items: items.map((item) => ({
type: Constants.ComponentTypes.FILE, type: Constants.ComponentTypes.FILE,
@@ -265,7 +271,7 @@ export interface FileOptions {
spoiler?: boolean; spoiler?: boolean;
} }
export const createFile = (url: string, spoiler?: boolean): FileComponent => ({ export const file = (url: string, spoiler?: boolean): FileComponent => ({
type: Constants.ComponentTypes.FILE, type: Constants.ComponentTypes.FILE,
file: { file: {
url, url,
@@ -282,7 +288,7 @@ export interface SeparatorOptions {
divider?: boolean; divider?: boolean;
spacing?: Padding; spacing?: Padding;
} }
export const createSeparator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({ export const separator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({
type: Constants.ComponentTypes.SEPARATOR, type: Constants.ComponentTypes.SEPARATOR,
divider, divider,
spacing: spacing ?? Padding.SMALL, spacing: spacing ?? Padding.SMALL,
@@ -301,14 +307,27 @@ export type ContainerItems =
| SeparatorComponent | SeparatorComponent
| FileComponent; | FileComponent;
export const createContainer = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({ export const container = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({
type: Constants.ComponentTypes.CONTAINER, type: Constants.ComponentTypes.CONTAINER,
...options, ...options,
components, components,
}); });
export const createModalLabel = (label: string, component: TextInput | StringSelectMenu): LabelComponent => ({ // Modals
export const label = (label: string, component: LabelComponent['component']): LabelComponent => ({
type: Constants.ComponentTypes.LABEL, type: Constants.ComponentTypes.LABEL,
label, label,
component, component,
}); });
export const modal = (
options: { custom_id?: string; title?: string },
...components: Array<LabelComponent | ActionRow | TextDisplayComponent>
): ModalSubmitInteractionData =>
({
type: 9 as any, // Modal type
custom_id: options.custom_id ?? '',
title: options.title ?? '',
components,
} as any);

View File

@@ -2,7 +2,7 @@ import {
Constants, Constants,
type ComponentBase, type ComponentBase,
type ModalSubmitInteractionDataLabelComponent, type ModalSubmitInteractionDataLabelComponent,
type ModalSubmitInteractionDataStringSelectComponent, type ModalSubmitInteractionDataSelectComponent,
type ModalSubmitInteractionDataTextInputComponent, type ModalSubmitInteractionDataTextInputComponent,
} from '@projectdysnomia/dysnomia'; } from '@projectdysnomia/dysnomia';
@@ -14,7 +14,7 @@ export function isModalTextInput(component: ComponentBase): component is ModalSu
return component.type === Constants.ComponentTypes.TEXT_INPUT; 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; return component.type === Constants.ComponentTypes.STRING_SELECT;
} }

View File

@@ -0,0 +1,2 @@
export * from './text';
export * from './locale';

View File

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

View File

@@ -15,10 +15,10 @@ export interface DiscordBotOptions {
onReady?: () => void; onReady?: () => void;
} }
export function startDiscordBot({ export function startBot({
token = process.env.DISCORD_BOT_TOKEN || '', token = process.env.DISCORD_BOT_TOKEN || '',
intents = [], intents = [],
commandPattern = '**/*.command.{js,ts}', commandPattern = '**/*.command.{js,ts,jsx,tsx}',
commandBaseDir = 'src', commandBaseDir = 'src',
keyStore = asyncKV, keyStore = asyncKV,
cache = kv, cache = kv,

View File

@@ -1,6 +1,7 @@
export * from './locales'; export * from './constants';
export * from './commands'; export * from './commands';
export * from './core'; export * from './core';
export * from './jsx'; export * from './jsx';
export * from './components'; export * from './components';
export * from './pages'; export * from './pages';
export * from './types';

View File

@@ -57,7 +57,7 @@ const JSD_INTERPOLATION = /\{(.+)\}/gs;
const JSD_START_EXP_INTERPOLATION = /\{(.+)\(/gs; const JSD_START_EXP_INTERPOLATION = /\{(.+)\(/gs;
const JSD_END_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); let interpolations = text.match(JSD_INTERPOLATION);
if (!interpolations) { if (!interpolations) {
if (text.match(JSD_START_EXP_INTERPOLATION)) { if (text.match(JSD_START_EXP_INTERPOLATION)) {

View File

@@ -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[] }) { export function ActionRow(props: { children: ActionRowElement['children'] }) {
return createActionRow(...(Array.isArray(props.children) ? props.children : [props.children])); return actionRow(...(Array.isArray(props.children) ? props.children : [props.children]));
} }

View File

@@ -1,6 +1,14 @@
import { createButton, type ButtonStyle } from '@/discord/components'; import { button, premiumButton, urlButton } from '@/discord/components';
import type { PartialEmoji } from '@projectdysnomia/dysnomia'; import type { ButtonElement, PremiumButtonElement, URLButtonElement } from './element.types';
export function Button(props: { label: string; customId: string; style: ButtonStyle; emoji?: PartialEmoji; disabled?: boolean }) { export function Button(props: ButtonElement['props']) {
return createButton(props.label, props.customId, { style: props.style, emoji: props.emoji, disabled: props.disabled }); 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 });
} }

View File

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

View File

@@ -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[] }) { export function Container(props: ContainerElement['props'] & { children: ContainerElement['children'] }) {
return createContainer( return container(
{ accent_color: props.accent, spoiler: props.spoiler }, { accent_color: props.accent, spoiler: props.spoiler },
...(Array.isArray(props.children) ? props.children : [props.children]), ...(Array.isArray(props.children) ? props.children : [props.children]),
); );

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,19 @@
export * from './action-row'; export * from './action-row';
export * from './button'; export * from './button';
export * from './channel-select';
export * from './container'; 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-display';
export * from './text-input';
export * from './thumbnail';
export * from './user-select';

View File

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

View File

@@ -0,0 +1,5 @@
import type { MediaElement } from './element.types';
export function Media(props: MediaElement['props']) {
return props;
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
import type { OptionElement } from './element.types';
export function Option(props: OptionElement['props']) {
return props;
}

View File

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

View File

@@ -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[]));
}

View File

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

View File

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

View File

@@ -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 }) { export function TextDisplay(props: TextDisplayElement['props'] & { children: TextDisplayElement['children'] }) {
return createTextDisplay(props.content); const children = Array.isArray(props.children) ? props.children.join('') : props.children;
return text(children);
} }

View File

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

View File

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

View File

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

View File

@@ -1 +1,2 @@
export { jsxDEV } from './runtime'; export { jsxDEV } from './runtime';
export type { JSX } from './jsx';

View File

@@ -1 +1,2 @@
export { jsx } from './runtime'; export { jsx } from './runtime';
export type { JSX } from './jsx';

View File

@@ -3,7 +3,6 @@ import {
type Button, type Button,
type ChannelSelectMenu, type ChannelSelectMenu,
type MentionableSelectMenu, type MentionableSelectMenu,
type PartialEmoji,
type RoleSelectMenu, type RoleSelectMenu,
type StringSelectMenu, type StringSelectMenu,
type TextInput, type TextInput,
@@ -19,7 +18,30 @@ import {
type URLButton, type URLButton,
type PremiumButton, type PremiumButton,
type ThumbnailComponent, type ThumbnailComponent,
type ModalSubmitInteractionData,
} from '@projectdysnomia/dysnomia'; } 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 = export type Component =
| ActionRow | ActionRow
@@ -40,30 +62,51 @@ export type Component =
| InteractionButton | InteractionButton
| URLButton | URLButton
| PremiumButton | PremiumButton
| ThumbnailComponent; | ThumbnailComponent
| ModalSubmitInteractionData;
export type Element = Component | Promise<Component>; export type StarKittenElement = Component | Promise<Component>;
export interface ElementClass { export interface StarKittenElementClass {
render: any; render: any;
} }
export interface ElementAttributesProperty { export interface StarKittenElementAttributesProperty {
props: {}; props: {};
} }
export interface IntrinsicElements { export interface StarKittenElementChildrenAttribute {
// Allow any element, but prefer known elements children: {};
// [elemName: string]: any; }
// Known elements (forcing re-parse)
actionRow: { children: any | any[] }; export interface StarKittenIntrinsicElements {
button: { actionRow: { children: StarKittenElement | StarKittenElement[] };
label: string; button: ButtonElement['props'];
customId: string; urlButton: URLButtonElement['props'];
style: number; premiumButton: PremiumButtonElement['props'];
emoji?: PartialEmoji; modal: ModalElement['props'] & { children: StarKittenElement | StarKittenElement[] };
disabled?: boolean; label: LabelElement['props'] & { children: StarKittenElement | StarKittenElement[] };
}; stringSelect: StringSelectElement['props'] & { children: StringSelectElement['children'] };
container: { color?: string; accent?: number; spoiler?: boolean; children: any | any[] }; option: OptionElement['props'];
textDisplay: { content: string }; 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 {}
} }

View File

@@ -1,17 +1,30 @@
import { ActionRow } from './components/action-row'; import * as components from './components';
import { Button } from './components/button';
import { Container } from './components/container';
import { TextDisplay } from './components/text-display';
const intrinsicComponentMap: Record<string, (props: any) => any> = { const intrinsicComponentMap: Record<string, (props: any) => any> = {
actionRow: ActionRow, actionRow: components.ActionRow,
button: Button, button: components.Button,
container: Container, container: components.Container,
textDisplay: TextDisplay, 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<string, any>) { export function jsx(type: any, props: Record<string, any>) {
console.log('JSX', type, props); // console.log('JSX', type, props);
if (typeof type === 'function') { if (typeof type === 'function') {
return type(props); return type(props);
} }
@@ -34,7 +47,7 @@ export function jsxDEV(
source: any, source: any,
self: any, self: any,
) { ) {
console.log('JSX DEV', type, props); // console.log('JSX DEV', type, props);
if (typeof type === 'function') { if (typeof type === 'function') {
return type(props); return type(props);
} }

View File

@@ -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';
export declare namespace JSX {
// type Element = Component;
interface ElementClass {
render: any;
}
interface ElementAttributesProperty {
props: {};
}
interface ElementChildrenAttribute {
children: {};
}
declare global {
namespace JSX {
type Element = Component;
interface IntrinsicElements extends StarKittenIntrinsicElements {} interface IntrinsicElements extends StarKittenIntrinsicElements {}
} }
}

View File

@@ -58,7 +58,7 @@ function createPageContext<T>(interaction: PagesInteraction, options: PagesOptio
custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : options.initialPage ?? 'root', custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : options.initialPage ?? 'root',
goToPage: (pageKey: string) => { goToPage: (pageKey: string) => {
const page = options.pages[pageKey]; const page = options.pages[pageKey];
this.state.currentPage = pageKey; state.currentPage = pageKey;
if (!page) { if (!page) {
throw new Error(`Page with key "${pageKey}" not found`); throw new Error(`Page with key "${pageKey}" not found`);
} }

View File

@@ -1,5 +1,5 @@
import type { PartialEmoji } from '@projectdysnomia/dysnomia'; 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'; import type { PageContext } from './pages';
export function getSubrouteKey(prefix: string, subroutes: string[]) { export function getSubrouteKey(prefix: string, subroutes: string[]) {
@@ -34,7 +34,7 @@ export function renderSubrouteButtons(
.map(({ label, value, emoji }) => { .map(({ label, value, emoji }) => {
const routes = [...subRoutes]; const routes = [...subRoutes];
routes[subrouteIndex] = currentSubroute == value ? '_' : value; routes[subrouteIndex] = currentSubroute == value ? '_' : value;
return createButton(label, getSubrouteKey(prefix, routes), { return button(label, getSubrouteKey(prefix, routes), {
...options, ...options,
disabled: value === currentSubroute, disabled: value === currentSubroute,
emoji, emoji,
@@ -76,10 +76,10 @@ export function renderSubroutes<T, CType = ContainerItems>(
const current = opts[sri] || opts[0]; const current = opts[sri] || opts[0];
const components = []; 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) { if (current.banner) {
components.push(createMediaGallery({ url: current.banner })); components.push(gallery({ url: current.banner }));
} }
if (!current.actionRowPosition || current.actionRowPosition === 'top') { if (!current.actionRowPosition || current.actionRowPosition === 'top') {

View File

@@ -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<T = any> {
id: string; // unique id for this command instance
name: string; // command name
data: T; // internal data storage
}
export interface PartialContext<T = any> {
client: Client;
cache: Cache;
kv: KVStore;
id?: string; // unique id for this command instance
state?: CommandState<T>; // state associated with this command instance
}
export type CommandContext<T = any> = Required<PartialContext<T>>;
export type ChatCommandDefinition = Omit<ChatInputApplicationCommandStructure, 'type'>;
export interface CommandHandler<T extends ApplicationCommandStructure> {
definition: T;
execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise<void>;
}

View File

@@ -0,0 +1,2 @@
export * from './command.type';
export * from './interaction.type';

View File

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

View File

@@ -91,7 +91,7 @@ export async function refresh(
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(`${options.client_id}:${options.client_secret}`).toString('base64')}`, Authorization: `Basic ${Buffer.from(`${options.client_id}:${options.client_secret}`).toString('base64')}`,
}, },
body: new URLSearchParams(params), body: new URLSearchParams(params as Record<string, string>),
}); });
return (await response.json()) as EveTokens; return (await response.json()) as EveTokens;
} }

View File

@@ -13,14 +13,23 @@ export const LOCALE_NAMES: { [key in Locales]: string } = {
}; };
export function toDiscordLocale(locale: Locales): string { export function toDiscordLocale(locale: Locales): string {
switch (locale) { switch (locale) {
case 'en': return 'en-US'; case 'en':
case 'ru': return 'ru'; return 'en-US';
case 'de': return 'de'; case 'ru':
case 'fr': return 'fr'; return 'ru';
case 'ja': return 'ja'; case 'de':
case 'es': return 'es-ES'; return 'de';
case 'zh': return 'zh-CN'; case 'fr':
case 'ko': return 'ko'; return 'fr';
default: return 'en-US'; case 'ja':
return 'ja';
case 'es':
return 'es-ES';
case 'zh':
return 'zh-CN';
case 'ko':
return 'ko';
default:
return 'en-US';
} }
} }

View File

@@ -2,7 +2,9 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"strict": true, "strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "@star-kitten/lib/discord", "jsxImportSource": "@star-kitten/lib/discord",
"lib": ["ESNext", "DOM"], "lib": ["ESNext", "DOM"],
@@ -12,11 +14,14 @@
"@data/*": ["./data/*"], "@data/*": ["./data/*"],
"@types/*": ["./types/*"] "@types/*": ["./types/*"]
}, },
"emitDeclarationOnly": true,
"noEmit": false, "noEmit": false,
"noEmitOnError": false,
"declaration": true, "declaration": true,
"outDir": "dist", "outDir": "dist/types",
"allowImportingTsExtensions": false "rootDir": "src",
"allowImportingTsExtensions": true
}, },
"include": ["src", "types", "src/jsx/types.d.ts"], "include": ["src", "types", "src/jsx/types.d.ts"],
"exclude": ["node_modules", "dist", "build", "**/*.test.ts"] "exclude": ["node_modules", "build", "**/*.test.ts"]
} }

View File

@@ -18,6 +18,8 @@ export default defineConfig([
], ],
platform: 'node', platform: 'node',
dts: true, dts: true,
minify: false,
mangle: false,
external: ['bun:sqlite', 'bun'], external: ['bun:sqlite', 'bun'],
}, },
]); ]);

View File

@@ -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<Component>;
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 };
}
}

View File

@@ -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 (
<container>
<textDisplay>
{`### ${eveTimeText[locale] || eveTimeText[Locale.EN_US]}
${eveTime}
${eveDate}`}
</textDisplay>
</container>
);
}
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 (
<modal customId="appraisalResult" title="Appraise Items">
<label label="Select a market">
<stringSelect customId="market" placeholder="Select a market" minValues={1} maxValues={1}>
{markets.map((m) => (
<option
label={m.name}
value={m.id.toString()}
default={m.id === 2} // Jita
/>
))}
</stringSelect>
</label>
<label label="Enter items to appraise">
<textInput
customId="input"
isParagraph={true}
placeholder={`e.g. Tritanium 22222
Pyerite 8000
Mexallon 2444`}
/>
</label>
</modal>
);
}
```
### 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`,
},
},
],
};
}
```

View File

@@ -1,12 +1,12 @@
# Star Kitten Discord Bot # 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 ## 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_ _Linux & MacOS_
@@ -22,44 +22,22 @@ powershell -c "irm bun.sh/install.ps1 | iex"
--- ---
Install dependencies. ### Install bot dependencies
```bash ```bash
bun install bun install
``` ```
### Link the Library & download static data ### Download static eve reference data & Hoboleaks archive from [EVE Ref](https://everef.net/)
`star-kitten-lib` has not been published, so link to it locally before running this web project.
```bash ```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 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
Run the bot locally.
```bash ```bash
bun run dev bun dev
``` ```
## Environment Variables ## Environment Variables
@@ -67,6 +45,9 @@ bun run dev
Create a .env file in the root directory with the following values: Create a .env file in the root directory with the following values:
```yaml ```yaml
# Discord - https://discord.com/developers/applications
DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN
#General #General
BASE_URL=http://localhost:3000 BASE_URL=http://localhost:3000
DEBUG=true DEBUG=true
@@ -74,26 +55,13 @@ PORT=3000
NODE_ENV=development NODE_ENV=development
LOG_LEVEL=debug LOG_LEVEL=debug
# EVE - https://developers.eveonline.com/applications # EVE - https://developers.eveonline.com/applications
EVE_CLIENT_ID=YOUR_EVE_CLIENT_ID EVE_CLIENT_ID=YOUR_EVE_CLIENT_ID
EVE_CLIENT_SECRET=YOUR_EVE_SECRET EVE_CLIENT_SECRET=YOUR_EVE_SECRET
EVE_CALLBACK_URL=http://localhost:3000/auth/callback EVE_CALLBACK_URL=http://localhost:3000/auth/callback
ESI_USER_AGENT=ADD_YOUR_USER_AGENT_INFO_HERE 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 # For using Janice's Appraisal API
JANICE_KEY=XXX JANICE_KEY=XXX
# For using Perplexities AI API
PERPLEXITY_API_KEY=XXX
``` ```

View File

@@ -47,7 +47,7 @@ async function execute(interaction: ExecutableInteraction, ctx: CommandContext)
appraiseModal: { appraiseModal: {
key: 'appraiseModal', key: 'appraiseModal',
type: PageType.MODAL, type: PageType.MODAL,
render: async () => renderAppraisalModal(interaction), render: () => renderAppraisalModal(interaction) as any,
}, },
appraisalResult: { appraisalResult: {
key: 'appraisalResult', key: 'appraisalResult',

View File

@@ -1,6 +1,6 @@
import type { ExecutableInteraction } from '@star-kitten/lib/discord'; import type { ExecutableInteraction } from '@star-kitten/lib/discord';
import * as StarKitten 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 { PageContext } from '@star-kitten/lib/discord/pages';
import { type Appraisal } from '@star-kitten/lib/eve/third-party/janice.js'; import { type Appraisal } from '@star-kitten/lib/eve/third-party/janice.js';
import { formatNumberToShortForm } from '@star-kitten/lib/util/text.js'; import { formatNumberToShortForm } from '@star-kitten/lib/util/text.js';

View File

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

View File

@@ -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 (
<modal customId="appraisalResult" title="Appraise Items">
<label label="Select a market">
<stringSelect customId="market" placeholder="Select a market" minValues={1} maxValues={1}>
{markets.map((m) => (
<option
label={m.name}
value={m.id.toString()}
default={m.id === 2} // Jita
/>
))}
</stringSelect>
</label>
<label label="Enter items to appraise">
<textInput
customId="input"
isParagraph={true}
placeholder={`e.g. Tritanium 22222
Pyerite 8000
Mexallon 2444`}
/>
</label>
</modal>
);
}

View File

@@ -2,11 +2,11 @@ import { renderSubroutes, type Page } from '@star-kitten/lib/discord/pages';
import type { SearchState } from '../search.command'; import type { SearchState } from '../search.command';
import { import {
ButtonStyle, ButtonStyle,
createContainer, container,
createSection, section,
createSeparator, separator,
createTextDisplay, text,
createThumbnail, thumbnail,
Padding, Padding,
} from '@star-kitten/lib/discord/components'; } from '@star-kitten/lib/discord/components';
import { import {
@@ -88,11 +88,11 @@ const page: Page<SearchState> = {
return { return {
components: [ components: [
createContainer( container(
{}, {},
createSection( section(
createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`), thumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
createTextDisplay(`# [${type.name.en}](https://everef.net/types/${type.type_id})\n## Attributes`), text(`# [${type.name.en}](https://everef.net/types/${type.type_id})\n## Attributes`),
), ),
...renderSubroutes( ...renderSubroutes(
context, context,
@@ -125,11 +125,11 @@ const page: Page<SearchState> = {
const unit = attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : ''; const unit = attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : '';
lines.push(`${attr.attribute.display_name.en.padEnd(24)} ${unit}`); 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 }, { style: ButtonStyle.SECONDARY },
), ),
createSeparator(Padding.LARGE), separator(Padding.LARGE),
searchActionRow('attributes'), searchActionRow('attributes'),
), ),
], ],

View File

@@ -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) { export function searchActionRow(pageKey: string) {
return createActionRow( return actionRow(
createButton('Main', 'main', { disabled: pageKey === 'main' }), button('Main', 'main', { disabled: pageKey === 'main' }),
createButton('Attributes', 'attributes', { disabled: pageKey === 'attributes' }), button('Attributes', 'attributes', { disabled: pageKey === 'attributes' }),
createButton('Fittings', 'fittings', { disabled: pageKey === 'fittings' }), button('Fittings', 'fittings', { disabled: pageKey === 'fittings' }),
createButton('Skills', 'skills', { disabled: pageKey === 'skills' }), button('Skills', 'skills', { disabled: pageKey === 'skills' }),
createButton('Industry', 'industry', { disabled: pageKey === 'industry' }), button('Industry', 'industry', { disabled: pageKey === 'industry' }),
); );
} }

View File

@@ -1,13 +1,6 @@
import type { Page } from '@star-kitten/lib/discord/pages'; import type { Page } from '@star-kitten/lib/discord/pages';
import type { SearchState } from '../search.command'; import type { SearchState } from '../search.command';
import { import { container, gallery, section, text, thumbnail, urlButton } from '@star-kitten/lib/discord/components';
createContainer,
createMediaGallery,
createSection,
createTextDisplay,
createThumbnail,
createURLButton,
} from '@star-kitten/lib/discord/components';
import { getRoleBonuses, getSkillBonuses, getType } from '@star-kitten/lib/eve/models/type.js'; import { getRoleBonuses, getSkillBonuses, getType } from '@star-kitten/lib/eve/models/type.js';
import { cleanText } from '@star-kitten/lib/eve/utils/markdown.js'; import { cleanText } from '@star-kitten/lib/eve/utils/markdown.js';
import { typeSearch } from '@star-kitten/lib/eve/utils/typeSearch.js'; import { typeSearch } from '@star-kitten/lib/eve/utils/typeSearch.js';
@@ -25,7 +18,7 @@ const page: Page<SearchState> = {
if (!found) { if (!found) {
return { return {
components: [createTextDisplay(`No item found for: ${typeName}`)], components: [text(`No item found for: ${typeName}`)],
}; };
} }
@@ -40,11 +33,11 @@ const page: Page<SearchState> = {
return { return {
components: [ components: [
createContainer( container(
{}, {},
createSection( section(
createThumbnail(`https://images.evetech.net/types/${type.type_id}/icon`), thumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
createTextDisplay(` text(`
# [${type.name.en}](https://everef.net/types/${type.type_id}) # [${type.name.en}](https://everef.net/types/${type.type_id})
${skillBonuses ${skillBonuses
@@ -67,18 +60,18 @@ ${roleBonuses
} }
`), `),
), ),
createMediaGallery({ gallery({
url: 'https://iili.io/KTPCFRt.md.webp', url: 'https://iili.io/KTPCFRt.md.webp',
}), }),
// createSeparator(Padding.LARGE), // createSeparator(Padding.LARGE),
createSection( section(
createURLButton('View on EVE Tycoon', `https://evetycoon.com/market/${type.type_id}`), urlButton('View on EVE Tycoon', `https://evetycoon.com/market/${type.type_id}`),
createTextDisplay( text(
`## Buy: ${price ? formatNumberToShortForm(price.buyAvgFivePercent) : '--'} ISK `## Buy: ${price ? formatNumberToShortForm(price.buyAvgFivePercent) : '--'} ISK
## Sell: ${price ? formatNumberToShortForm(price.sellAvgFivePercent) : '--'} ISK`, ## Sell: ${price ? formatNumberToShortForm(price.sellAvgFivePercent) : '--'} ISK`,
), ),
), ),
createTextDisplay(`-# Type Id: ${type.type_id}`), text(`-# Type Id: ${type.type_id}`),
searchActionRow('main'), searchActionRow('main'),
), ),
], ],

View File

@@ -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 (
<container>
<textDisplay>
{`### ${eveTimeText[locale] || eveTimeText[Locale.EN_US]}
${eveTime}
${eveDate}`}
</textDisplay>
</container>
);
}
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,
};

View File

@@ -1,3 +1,3 @@
import { startDiscordBot } from '@star-kitten/lib/discord'; import { startBot } from '@star-kitten/lib/discord';
startDiscordBot(); startBot();

View File

@@ -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 = (
<actionRow>
<container accent={0x1da57a}>
<textDisplay content={`Hello ${world}`} />
{pageCtx.state.currentPage !== 'share' ? (
<actionRow>
<button customId="share" label="Share in Channel" disabled={rand < 500} />
</actionRow>
) : undefined}
</container>
</actionRow>
);
console.log(jsx);
}
renderAppraisal();

View File

@@ -4,9 +4,9 @@
"composite": true, "composite": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "@star-kitten/lib/discord", "jsxImportSource": "@star-kitten/lib/discord",
"moduleResolution": "bundler",
"paths": { "paths": {
"@*": ["./src/*"], "@/*": ["./src/*"]
"@star-kitten/lib/*": ["../lib/src/*"]
}, },
"typeRoots": ["src/types", "./node_modules/@types"] "typeRoots": ["src/types", "./node_modules/@types"]
}, },

127
types/jsx.d.ts vendored
View File

@@ -53,25 +53,48 @@ declare namespace JSX {
props: {}; props: {};
} }
// This tells TypeScript to check children against the 'children' property
interface ElementChildrenAttribute {
children: {};
}
interface IntrinsicElements { interface IntrinsicElements {
// Allow any element, but prefer known elements // Known elements - no index signature to enforce type safety
[elemName: string]: any; actionRow: { children: Element | Element[] };
// Known elements button: {
ActionRow: { children: any | any[] };
Button: {
label: string; label: string;
customId: string; customId: string;
style?: number; style: number;
emoji?: PartialEmoji; emoji?: PartialEmoji;
disabled?: boolean; disabled?: boolean;
}; };
Container: { accent?: number; spoiler?: boolean; children: any | any[] }; container: {
TextDisplay: { content: string }; accent?: number;
spoiler?: boolean;
children: Element | Element[];
};
textDisplay: { content: string };
modal: { customId?: string; title?: string; children: Element | Element[] };
label: { label: string; children: Element };
stringSelect: {
customId: string;
placeholder?: string;
minValues?: number;
maxValues?: number;
children: Element | Element[];
};
option: {
label: string;
value: string;
description?: string;
emoji?: PartialEmoji;
default?: boolean;
};
} }
} }
declare module "@star-kitten/lib/discord/jsx-runtime" { declare module "@star-kitten/lib/discord/jsx-runtime" {
declare namespace JSX { export namespace JSX {
type Component = type Component =
| Button | Button
| StringSelectMenu | StringSelectMenu
@@ -102,26 +125,52 @@ declare module "@star-kitten/lib/discord/jsx-runtime" {
props: {}; props: {};
} }
// This tells TypeScript to check children against the 'children' property
interface ElementChildrenAttribute {
children: {};
}
interface IntrinsicElements { interface IntrinsicElements {
// Allow any element, but prefer known elements actionRow: { children: Element | Element[] };
[elemName: string]: any; button: {
// Known elements
ActionRow: { children: any | any[] };
Button: {
label: string; label: string;
customId: string; customId: string;
style?: number; style: number;
emoji?: PartialEmoji; emoji?: PartialEmoji;
disabled?: boolean; disabled?: boolean;
}; };
Container: { accent?: number; spoiler?: boolean; children: any | any[] }; container: {
TextDisplay: { content: string }; accent?: number;
spoiler?: boolean;
children: Element | Element[];
};
textDisplay: { content: string };
modal: {
customId?: string;
title?: string;
children: Element | Element[];
};
label: { label: string; children: Element };
stringSelect: {
customId: string;
placeholder?: string;
minValues?: number;
maxValues?: number;
children: Element | Element[];
};
option: {
label: string;
value: string;
description?: string;
emoji?: PartialEmoji;
default?: boolean;
};
} }
} }
} }
declare module "@star-kitten/lib/discord/jsx-dev-runtime" { declare module "@star-kitten/lib/discord/jsx-dev-runtime" {
declare namespace JSX { export namespace JSX {
type Component = type Component =
| Button | Button
| StringSelectMenu | StringSelectMenu
@@ -152,20 +201,46 @@ declare module "@star-kitten/lib/discord/jsx-dev-runtime" {
props: {}; props: {};
} }
// This tells TypeScript to check children against the 'children' property
interface ElementChildrenAttribute {
children: {};
}
interface IntrinsicElements { interface IntrinsicElements {
// Allow any element, but prefer known elements actionRow: { children: Element | Element[] };
[elemName: string]: any; button: {
// Known elements
ActionRow: { children: any | any[] };
Button: {
label: string; label: string;
customId: string; customId: string;
style?: number; style: number;
emoji?: PartialEmoji; emoji?: PartialEmoji;
disabled?: boolean; disabled?: boolean;
}; };
Container: { accent?: number; spoiler?: boolean; children: any | any[] }; container: {
TextDisplay: { content: string }; accent?: number;
spoiler?: boolean;
children: Element | Element[];
};
textDisplay: { content: string };
modal: {
customId?: string;
title?: string;
children: Element | Element[];
};
label: { label: string; children: Element };
stringSelect: {
customId: string;
placeholder?: string;
minValues?: number;
maxValues?: number;
children: Element | Element[];
};
option: {
label: string;
value: string;
description?: string;
emoji?: PartialEmoji;
default?: boolean;
};
} }
} }
} }