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

View File

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

View File

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

View File

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

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 {
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<SearchState> = {
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<SearchState> = {
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'),
),
],

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

View File

@@ -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<SearchState> = {
if (!found) {
return {
components: [createTextDisplay(`No item found for: ${typeName}`)],
components: [text(`No item found for: ${typeName}`)],
};
}
@@ -40,11 +33,11 @@ const page: Page<SearchState> = {
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'),
),
],

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,
"jsx": "react-jsx",
"jsxImportSource": "@star-kitten/lib/discord",
"moduleResolution": "bundler",
"paths": {
"@*": ["./src/*"],
"@star-kitten/lib/*": ["../lib/src/*"]
"@/*": ["./src/*"]
},
"typeRoots": ["src/types", "./node_modules/@types"]
},