Initial commit
This commit is contained in:
8
packages/lib/.prettierrc.yaml
Normal file
8
packages/lib/.prettierrc.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
trailingComma: all
|
||||
tabWidth: 2
|
||||
useTabs: false
|
||||
semi: true
|
||||
singleQuote: true
|
||||
printWidth: 140
|
||||
experimentalTernaries: true
|
||||
quoteProps: consistent
|
||||
23
packages/lib/README.md
Normal file
23
packages/lib/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# tsdown-starter
|
||||
|
||||
A starter for creating a TypeScript package.
|
||||
|
||||
## Development
|
||||
|
||||
- Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
- Run the unit tests:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
- Build the library:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
9
packages/lib/build.ts
Normal file
9
packages/lib/build.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const bundle = await Bun.build({
|
||||
entrypoints: ['./src/***.ts', '!./src/**/*.test.ts'],
|
||||
outdir: 'dist',
|
||||
minify: true,
|
||||
});
|
||||
|
||||
if (!bundle.success) {
|
||||
throw new AggregateError(bundle.logs);
|
||||
}
|
||||
7
packages/lib/bunfig.toml
Normal file
7
packages/lib/bunfig.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[test]
|
||||
coverage = true
|
||||
coverageSkipTestFiles = true
|
||||
coverageReporter = ["text", "lcov"]
|
||||
|
||||
[run]
|
||||
bun = true
|
||||
4
packages/lib/data/.gitignore
vendored
Normal file
4
packages/lib/data/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
7
packages/lib/drizzle.config.ts
Normal file
7
packages/lib/drizzle.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
schema: "./src/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
});
|
||||
8
packages/lib/fixtures/commands/test1.command.ts
Normal file
8
packages/lib/fixtures/commands/test1.command.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { CommandHandler } from '@/commands/command-handler.type';
|
||||
|
||||
const handler: CommandHandler<{ name: string; type: 1; description: string }> = {
|
||||
definition: { name: 'test1', type: 1, description: 'Test command 1' },
|
||||
execute: async () => {},
|
||||
};
|
||||
|
||||
export default handler;
|
||||
8
packages/lib/fixtures/commands/test2.command.ts
Normal file
8
packages/lib/fixtures/commands/test2.command.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { CommandHandler } from '@/commands/command-handler.type';
|
||||
|
||||
const handler: CommandHandler<{ name: string; type: 1; description: string }> = {
|
||||
definition: { name: 'test2', type: 1, description: 'Test command 2' },
|
||||
execute: async () => {},
|
||||
};
|
||||
|
||||
export default handler;
|
||||
30
packages/lib/fixtures/jsd/test.ts
Normal file
30
packages/lib/fixtures/jsd/test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as StarKitten from '@star-kitten/lib/discord';
|
||||
import type { ExecutableInteraction } from '@star-kitten/lib/discord';
|
||||
import { createActionRow, createButton, createContainer, createTextDisplay } 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';
|
||||
|
||||
export function renderAppraisal(appraisal: Appraisal, pageCtx: PageContext<any>, interaction: ExecutableInteraction) {
|
||||
const formatter = new Intl.NumberFormat(interaction.locale || 'en-US', {
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
const world = 'world';
|
||||
return StarKitten.createElement(
|
||||
'ActionRow',
|
||||
{},
|
||||
StarKitten.createElement(
|
||||
'Container',
|
||||
{ color: '0x1da57a' },
|
||||
StarKitten.createElement('TextDisplay', {}, '' + `Hello ${world}` + ''),
|
||||
pageCtx.state.currentPage !== 'share'
|
||||
? StarKitten.createElement(
|
||||
'ActionRow',
|
||||
{},
|
||||
StarKitten.createElement('Button', { key: 'share', disabled: '{!unknown}' }, 'Share in Channel'),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
32
packages/lib/fixtures/jsonQuery/test-data-array.json
Normal file
32
packages/lib/fixtures/jsonQuery/test-data-array.json
Normal file
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Alice",
|
||||
"age": 30,
|
||||
"department": "Engineering"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Bob",
|
||||
"age": 25,
|
||||
"department": "Marketing"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Charlie",
|
||||
"age": 35,
|
||||
"department": "Engineering"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Diana",
|
||||
"age": 28,
|
||||
"department": "Sales"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Eve",
|
||||
"age": 32,
|
||||
"department": "Engineering"
|
||||
}
|
||||
]
|
||||
3
packages/lib/fixtures/jsonQuery/test-data-invalid.json
Normal file
3
packages/lib/fixtures/jsonQuery/test-data-invalid.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"invalid": "json",
|
||||
"missing": "closing brace"
|
||||
32
packages/lib/fixtures/jsonQuery/test-data-object.json
Normal file
32
packages/lib/fixtures/jsonQuery/test-data-object.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"users": {
|
||||
"alice": {
|
||||
"id": 1,
|
||||
"name": "Alice",
|
||||
"age": 30,
|
||||
"department": "Engineering"
|
||||
},
|
||||
"bob": {
|
||||
"id": 2,
|
||||
"name": "Bob",
|
||||
"age": 25,
|
||||
"department": "Marketing"
|
||||
}
|
||||
},
|
||||
"departments": {
|
||||
"engineering": {
|
||||
"name": "Engineering",
|
||||
"budget": 1000000,
|
||||
"headCount": 15
|
||||
},
|
||||
"marketing": {
|
||||
"name": "Marketing",
|
||||
"budget": 500000,
|
||||
"headCount": 8
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"version": "1.0.0",
|
||||
"environment": "test"
|
||||
}
|
||||
}
|
||||
29
packages/lib/fixtures/markdown/test-data-colors.json
Normal file
29
packages/lib/fixtures/markdown/test-data-colors.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"colors": ["red", "blue", "green", "yellow"],
|
||||
"testText": {
|
||||
"simple": "Hello World",
|
||||
"multiline": "Line 1\nLine 2\nLine 3",
|
||||
"withSpecialChars": "Text with !@#$%^&*()_+-=[]{}|;':\",./<>?",
|
||||
"empty": "",
|
||||
"unicode": "Unicode: 🌟 ❤️ 🔥",
|
||||
"code": "function test() { return 'hello'; }"
|
||||
},
|
||||
"expected": {
|
||||
"red": {
|
||||
"simple": "```ansi\n\u001b[2;31mHello World\u001b[0m```\n",
|
||||
"empty": "```ansi\n\u001b[2;31m\u001b[0m```\n"
|
||||
},
|
||||
"blue": {
|
||||
"simple": "```ansi\n\u001b[2;32m\u001b[2;36m\u001b[2;34mHello World\u001b[0m\u001b[2;36m\u001b[0m\u001b[2;32m\u001b[0m```\n",
|
||||
"empty": "```ansi\n\u001b[2;32m\u001b[2;36m\u001b[2;34m\u001b[0m\u001b[2;36m\u001b[0m\u001b[2;32m\u001b[0m```\n"
|
||||
},
|
||||
"green": {
|
||||
"simple": "```ansi\n\u001b[2;36mHello World\u001b[0m```\n",
|
||||
"empty": "```ansi\n\u001b[2;36m\u001b[0m```\n"
|
||||
},
|
||||
"yellow": {
|
||||
"simple": "```ansi\n\u001b[2;33mHello World\u001b[0m```\n",
|
||||
"empty": "```ansi\n\u001b[2;33m\u001b[0m```\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
41
packages/lib/fixtures/markdown/test-data-markup.json
Normal file
41
packages/lib/fixtures/markdown/test-data-markup.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"boldMarkup": {
|
||||
"complete": "<b>bold text</b>",
|
||||
"openOnly": "<b>bold text",
|
||||
"closeOnly": "bold text</b>",
|
||||
"nested": "<b>outer <b>inner</b> text</b>",
|
||||
"empty": "<b></b>",
|
||||
"multiple": "<b>first</b> and <b>second</b>",
|
||||
"mixed": "<b>bold</b> with <i>italic</i> text"
|
||||
},
|
||||
"italicMarkup": {
|
||||
"complete": "<i>italic text</i>",
|
||||
"openOnly": "<i>italic text",
|
||||
"closeOnly": "italic text</i>",
|
||||
"nested": "<i>outer <i>inner</i> text</i>",
|
||||
"empty": "<i></i>",
|
||||
"multiple": "<i>first</i> and <i>second</i>",
|
||||
"mixed": "<i>italic</i> with <b>bold</b> text"
|
||||
},
|
||||
"colorTags": {
|
||||
"hex6": "<color=0xFF5733>colored text</color>",
|
||||
"hex8": "<color=0xFF5733AA>colored text</color>",
|
||||
"hexWithoutPrefix": "<color=FF5733>colored text</color>",
|
||||
"namedColor": "<color=red>colored text</color>",
|
||||
"nested": "<color=blue>outer <color=red>inner</color> text</color>",
|
||||
"empty": "<color=green></color>",
|
||||
"multiple": "<color=red>first</color> and <color=blue>second</color>"
|
||||
},
|
||||
"eveLinks": {
|
||||
"simple": "<a href=showinfo:587>Rifter</a>",
|
||||
"withSpaces": "<a href=showinfo:12345>Ship Name With Spaces</a>",
|
||||
"multiple": "<a href=showinfo:587>Rifter</a> and <a href=showinfo:588>Merlin</a>",
|
||||
"nested": "Check out <a href=showinfo:587>Rifter</a> for PvP",
|
||||
"empty": "<a href=showinfo:587></a>"
|
||||
},
|
||||
"combined": {
|
||||
"allMarkup": "<b>Bold</b> <i>italic</i> <color=red>colored</color> <a href=showinfo:587>linked</a>",
|
||||
"nestedComplex": "<b><color=blue><a href=showinfo:587>Bold Blue Rifter</a></color></b>",
|
||||
"realWorldExample": "The <b><color=0xFF5733>Rifter</color></b> is a <i>fast</i> <a href=showinfo:587>frigate</a> used in PvP."
|
||||
}
|
||||
}
|
||||
34
packages/lib/fixtures/markdown/test-data-time.json
Normal file
34
packages/lib/fixtures/markdown/test-data-time.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"milliseconds": {
|
||||
"zero": 0,
|
||||
"oneSecond": 1000,
|
||||
"oneMinute": 60000,
|
||||
"oneHour": 3600000,
|
||||
"complex": 3661500,
|
||||
"daysWorthMs": 86400000,
|
||||
"fractionalSeconds": 1500,
|
||||
"smallFraction": 100
|
||||
},
|
||||
"seconds": {
|
||||
"zero": 0,
|
||||
"oneSecond": 1,
|
||||
"oneMinute": 60,
|
||||
"oneHour": 3600,
|
||||
"complex": 3661,
|
||||
"daysWorthSec": 86400,
|
||||
"fractionalInput": 3661.5
|
||||
},
|
||||
"expected": {
|
||||
"zero": "0.0s",
|
||||
"oneSecond": "1.0s",
|
||||
"oneMinute": "1m",
|
||||
"oneHour": "1h",
|
||||
"complexMs": "1h 1m 1.5s",
|
||||
"complexSec": "1h 1m 1s",
|
||||
"daysMs": "24h",
|
||||
"daysSec": "24h",
|
||||
"fractionalSeconds": "1.5s",
|
||||
"smallFraction": "0.1s",
|
||||
"fractionalInputSec": "1h 1m 1s"
|
||||
}
|
||||
}
|
||||
167
packages/lib/package.json
Normal file
167
packages/lib/package.json
Normal file
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"name": "@star-kitten/lib",
|
||||
"version": "0.0.0",
|
||||
"description": "Star Kitten Library.",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/author/library#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/author/library/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/author/library.git"
|
||||
},
|
||||
"author": "JB <j-b-3.deviate267@passmail.net>",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index*.d.ts"
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
"./util": {
|
||||
"types": "./src/util/index.d.ts",
|
||||
"require": "./src/util/index.js",
|
||||
"import": "./dist/util/index.js"
|
||||
},
|
||||
"./util/*": {
|
||||
"types": "./src/util/**/*.d.ts",
|
||||
"require": "./src/util/*",
|
||||
"import": "./dist/util/*"
|
||||
},
|
||||
"./eve": {
|
||||
"types": "./src/eve/index.d.ts",
|
||||
"require": "./src/eve/index.js",
|
||||
"import": "./dist/eve/index.js"
|
||||
},
|
||||
"./eve/*": {
|
||||
"types": "./src/eve/**/*.d.ts",
|
||||
"require": "./src/eve/*",
|
||||
"import": "./dist/eve/*"
|
||||
},
|
||||
"./eve/esi": {
|
||||
"import": "./dist/eve/esi/index.js",
|
||||
"types": "./src/eve/esi/index*.d.ts",
|
||||
"require": "./src/eve/esi/index.js"
|
||||
},
|
||||
"./eve/db": {
|
||||
"import": "./dist/eve/db/index.js",
|
||||
"types": "./src/eve/db/index*.d.ts",
|
||||
"require": "./src/eve/db/index.js"
|
||||
},
|
||||
"./eve/ref": {
|
||||
"import": "./dist/eve/ref/index.js",
|
||||
"types": "./src/eve/ref/index*.d.ts",
|
||||
"require": "./src/eve/ref/index.js"
|
||||
},
|
||||
"./eve/third-party/janice.js": {
|
||||
"import": "./dist/eve/third-party/janice.js",
|
||||
"types": "./dist/types/eve/third-party/janice.d.ts",
|
||||
"require": "./src/eve/third-party/janice.js"
|
||||
},
|
||||
"./eve/models": {
|
||||
"import": "./dist/eve/models/index.js",
|
||||
"types": "./src/eve/models/index*.d.ts",
|
||||
"require": "./src/eve/models/index.js"
|
||||
},
|
||||
"./eve/data/*": "./data/*",
|
||||
"./discord": {
|
||||
"import": "./dist/discord/index.js",
|
||||
"require": "./src/discord/index.js",
|
||||
"types": "./dist/types/discord/index.d.ts"
|
||||
},
|
||||
"./discord/commands": {
|
||||
"require": "./src/discord/commands/index.js",
|
||||
"import": "./dist/discord/commands/index.js",
|
||||
"types": "./dist/types/discord/commands/index.d.ts"
|
||||
},
|
||||
"./discord/components": {
|
||||
"types": "./dist/types/discord/components/index.d.ts",
|
||||
"require": "./src/discord/components/index.js",
|
||||
"import": "./dist/discord/components/index.js"
|
||||
},
|
||||
"./discord/pages": {
|
||||
"require": "./src/discord/pages/index.js",
|
||||
"import": "./dist/discord/pages/index.js",
|
||||
"types": "./dist/types/discord/pages/index.d.ts"
|
||||
},
|
||||
"./discord/common": {
|
||||
"require": "./src/discord/common/index.js",
|
||||
"import": "./dist/discord/common/index.js"
|
||||
},
|
||||
"./discord/jsx": {
|
||||
"types": "./src/discord/jsx/types.d.ts",
|
||||
"import": "./dist/discord/jsx/index.js"
|
||||
},
|
||||
"./discord/jsx-runtime": {
|
||||
"types": "./src/discord/jsx/types.d.ts",
|
||||
"default": "./dist/discord/jsx/jsx-runtime.js"
|
||||
},
|
||||
"./discord/jsx-dev-runtime": {
|
||||
"types": "./src/discord/jsx/types.d.ts",
|
||||
"default": "./dist/discord/jsx/jsx-dev-runtime.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.5",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/jwk-to-pem": "^2.0.3",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/node-cache": "^4.2.5",
|
||||
"@types/stream-chain": "^2.1.0",
|
||||
"@types/stream-json": "^1.7.8",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"bumpp": "^10.1.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"ghooks": "^2.0.4",
|
||||
"prettier-plugin-multiline-arrays": "^4.0.3",
|
||||
"tsdown": "^0.14.2",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@orama/orama": "^3.1.13",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"cron-parser": "^5.3.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"elysia": "^1.4.20",
|
||||
"fp-filters": "^0.5.4",
|
||||
"html-dom-parser": "^5.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwk-to-pem": "^2.0.7",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"node-cache": "^5.1.2",
|
||||
"stream-chain": "^3.4.0",
|
||||
"stream-json": "^1.9.1",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"release": "bumpp && npm publish",
|
||||
"generate-migrations": "bunx drizzle-kit generate --dialect sqlite --schema ./src/db/schema.ts",
|
||||
"migrate": "bun run ./src/db/migrate.ts",
|
||||
"postinstall": "bun get-data",
|
||||
"get-data": "bun refresh:reference-data && bun refresh:hoboleaks",
|
||||
"refresh:reference-data": "bun scripts/download-and-extract.ts https://data.everef.net/reference-data/reference-data-latest.tar.xz ./data/reference-data",
|
||||
"refresh:hoboleaks": "bun scripts/download-and-extract.ts https://data.everef.net/hoboleaks-sde/hoboleaks-sde-latest.tar.xz ./data/hoboleaks",
|
||||
"static-export": "bun scripts/export-solar-systems.ts"
|
||||
}
|
||||
}
|
||||
62
packages/lib/scripts/download-and-extract.ts
Normal file
62
packages/lib/scripts/download-and-extract.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
export async function downloadAndExtract(url: string, outputDir: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok || !response.body) throw new Error(`Failed to download ${url}`);
|
||||
const nodeStream = Readable.fromWeb(response.body as any);
|
||||
|
||||
const compressedFilePath = path.join(outputDir, 'archive.tar.xz');
|
||||
const fileStream = fs.createWriteStream(compressedFilePath);
|
||||
|
||||
nodeStream.pipe(fileStream);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fileStream.on('finish', () => {
|
||||
// Use native tar command to extract files
|
||||
exec(`tar -xJf ${compressedFilePath} -C ${outputDir}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Extraction error: ${stderr}`);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log('Extraction complete');
|
||||
|
||||
// Clean up the archive file
|
||||
fs.unlink(compressedFilePath, (err) => {
|
||||
if (err) {
|
||||
console.error(`Error removing archive: ${err.message}`);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Archive cleaned up');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
fileStream.on('error', (err) => {
|
||||
console.error('File stream error', err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// CLI execution (only runs when file is executed directly)
|
||||
if (import.meta.main) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length !== 2) {
|
||||
console.error('Usage: bun run downloadAndExtract.ts <url> <outputDir>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [url, outputDir] = args;
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
downloadAndExtract(url, outputDir).catch((err) => console.error('Download failed', err));
|
||||
}
|
||||
19
packages/lib/scripts/export-solar-systems.ts
Normal file
19
packages/lib/scripts/export-solar-systems.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { join } from "node:path";
|
||||
|
||||
const db = new Database(join(process.cwd(), 'data/evestatic.db'));
|
||||
|
||||
const query = db.query("SELECT * FROM mapSolarSystems");
|
||||
const results = query.all();
|
||||
|
||||
const output = results.reduce((acc: any, system: any) => {
|
||||
acc[system.solarSystemID] = system;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const jsonData = JSON.stringify(output, null, 2);
|
||||
|
||||
const fs = await import('fs/promises');
|
||||
await fs.writeFile(join(process.cwd(), 'data/reference-data/solar_systems.json'), jsonData);
|
||||
|
||||
db.close();
|
||||
15
packages/lib/src/discord/commands/command-handler.ts
Normal file
15
packages/lib/src/discord/commands/command-handler.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type ChatInputApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||
import type { ExecutableInteraction } from '../types/interaction.type';
|
||||
import type { ChatCommandDefinition, CommandContext, CommandHandler } from '../types';
|
||||
|
||||
export function createChatCommand(
|
||||
definition: ChatCommandDefinition,
|
||||
execute: (interaction: ExecutableInteraction, ctx: CommandContext) => Promise<void>,
|
||||
): CommandHandler<ChatInputApplicationCommandStructure> {
|
||||
const def = definition as ChatInputApplicationCommandStructure;
|
||||
def.type = 1; // CHAT_INPUT
|
||||
return {
|
||||
definition: def,
|
||||
execute,
|
||||
};
|
||||
}
|
||||
72
packages/lib/src/discord/commands/command-helpers.ts
Normal file
72
packages/lib/src/discord/commands/command-helpers.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Constants } from '@projectdysnomia/dysnomia';
|
||||
import type {
|
||||
CommandInteraction,
|
||||
ExecutableInteraction,
|
||||
Interaction,
|
||||
AutocompleteInteraction,
|
||||
ComponentInteraction,
|
||||
ModalSubmitInteraction,
|
||||
PingInteraction,
|
||||
} from '../types';
|
||||
|
||||
export function isApplicationCommand(interaction: Interaction): interaction is CommandInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND;
|
||||
}
|
||||
|
||||
export function isModalSubmit(interaction: Interaction): interaction is ModalSubmitInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.MODAL_SUBMIT;
|
||||
}
|
||||
|
||||
export function isMessageComponent(interaction: Interaction): interaction is ComponentInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.MESSAGE_COMPONENT;
|
||||
}
|
||||
|
||||
export function isAutocomplete(interaction: Interaction): interaction is AutocompleteInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE;
|
||||
}
|
||||
|
||||
export function isPing(interaction: Interaction): interaction is PingInteraction {
|
||||
return interaction.type === Constants.InteractionTypes.PING;
|
||||
}
|
||||
|
||||
export function commandHasName(interaction: Interaction, name: string): boolean {
|
||||
return isApplicationCommand(interaction) && interaction.data.name === name;
|
||||
}
|
||||
|
||||
export function commandHasIdPrefix(interaction: Interaction, prefix: string): boolean {
|
||||
return (isModalSubmit(interaction) || isMessageComponent(interaction)) && interaction.data.custom_id.startsWith(prefix);
|
||||
}
|
||||
|
||||
export function getCommandName(interaction: ExecutableInteraction): string | undefined {
|
||||
if (isApplicationCommand(interaction) || isAutocomplete(interaction)) {
|
||||
return interaction.data.name;
|
||||
}
|
||||
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;
|
||||
}
|
||||
99
packages/lib/src/discord/commands/command-injection.ts
Normal file
99
packages/lib/src/discord/commands/command-injection.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { type InteractionModalContent, type Component, Constants } from '@projectdysnomia/dysnomia';
|
||||
import type { CommandContext, PartialContext, ExecutableInteraction } from '../types';
|
||||
import { int } from 'drizzle-orm/mysql-core';
|
||||
import _ from 'lodash';
|
||||
|
||||
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.
|
||||
if (ctx.state.name) {
|
||||
if ('createModal' in interaction) {
|
||||
const _originalCreateModal = interaction.createModal.bind(interaction);
|
||||
interaction.createModal = (content: InteractionModalContent) => {
|
||||
validateCustomIdLength(content.custom_id);
|
||||
content.custom_id = `${content.custom_id}_${ctx.state.id}`;
|
||||
return _originalCreateModal(content);
|
||||
};
|
||||
|
||||
interaction.createJSXModal = async (component) => {
|
||||
return interaction.createModal(component as any);
|
||||
};
|
||||
}
|
||||
|
||||
if ('createMessage' in interaction) {
|
||||
const _originalCreateMessage = interaction.createMessage.bind(interaction);
|
||||
interaction.createMessage = (content) => {
|
||||
if (typeof content === 'string') return _originalCreateMessage(content);
|
||||
if (content.components) {
|
||||
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||
}
|
||||
return _originalCreateMessage(content);
|
||||
};
|
||||
|
||||
interaction.createJSXMessage = async (component) => {
|
||||
const messageContent = {
|
||||
flags: Constants.MessageFlags.IS_COMPONENTS_V2,
|
||||
components: [component],
|
||||
};
|
||||
return interaction.createMessage(messageContent as any);
|
||||
};
|
||||
}
|
||||
|
||||
if ('editMessage' in interaction) {
|
||||
const _originalEditMessage = interaction.editMessage.bind(interaction);
|
||||
interaction.editMessage = (messageID, content) => {
|
||||
if (typeof content === 'string') return _originalEditMessage(messageID, content);
|
||||
if (content.components) {
|
||||
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||
}
|
||||
return _originalEditMessage(messageID, content);
|
||||
};
|
||||
|
||||
interaction.editJSXMessage = async (messageID, component) => {
|
||||
const messageContent = {
|
||||
flags: Constants.MessageFlags.IS_COMPONENTS_V2,
|
||||
components: [component],
|
||||
};
|
||||
return interaction.editMessage(messageID, messageContent as any);
|
||||
};
|
||||
}
|
||||
|
||||
if ('createFollowup' in interaction) {
|
||||
const _originalCreateFollowup = interaction.createFollowup.bind(interaction);
|
||||
interaction.createFollowup = (content) => {
|
||||
if (typeof content === 'string') return _originalCreateFollowup(content);
|
||||
if (content.components) {
|
||||
addCommandIdToComponentCustomIds(content.components, ctx.state.id);
|
||||
}
|
||||
return _originalCreateFollowup(content);
|
||||
};
|
||||
|
||||
interaction.createJSXFollowup = async (component) => {
|
||||
const messageContent = {
|
||||
flags: Constants.MessageFlags.IS_COMPONENTS_V2,
|
||||
components: [component],
|
||||
};
|
||||
return interaction.createFollowup(messageContent as any);
|
||||
};
|
||||
}
|
||||
}
|
||||
return [interaction, ctx as CommandContext];
|
||||
}
|
||||
|
||||
function validateCustomIdLength(customId: string) {
|
||||
if (customId.length > 80) {
|
||||
throw new Error(`Custom ID too long: ${customId.length} characters (max 80) with this framework. Consider using shorter IDs.`);
|
||||
}
|
||||
}
|
||||
|
||||
function addCommandIdToComponentCustomIds(components: Component[], commandId: string) {
|
||||
components.forEach((component) => {
|
||||
if (!component) return;
|
||||
if ('custom_id' in component) {
|
||||
validateCustomIdLength(component.custom_id as string);
|
||||
component.custom_id = `${component.custom_id}_${commandId}`;
|
||||
}
|
||||
if ('components' in component && Array.isArray(component.components)) {
|
||||
addCommandIdToComponentCustomIds(component.components, commandId);
|
||||
}
|
||||
});
|
||||
}
|
||||
48
packages/lib/src/discord/commands/command-state.ts
Normal file
48
packages/lib/src/discord/commands/command-state.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createReactiveState } from '@/util/reactive-state.js';
|
||||
import { isApplicationCommand, isAutocomplete } from './command-helpers';
|
||||
import type { CommandState, ExecutableInteraction, PartialContext } from '../types';
|
||||
|
||||
export async function getCommandState<T>(interaction: ExecutableInteraction, ctx: PartialContext): Promise<CommandState<T>> {
|
||||
const id = instanceIdFromInteraction(interaction);
|
||||
|
||||
let state: CommandState<T>;
|
||||
// get state from kv store if possible
|
||||
if (ctx.kv.has(`command-state:${id}`)) {
|
||||
state = await ctx.kv.get<CommandState<T>>(`command-state:${id}`);
|
||||
}
|
||||
if (!state) {
|
||||
state = { id: id, name: '', data: {} as T };
|
||||
}
|
||||
const [reactiveState, subscribe] = createReactiveState(state);
|
||||
subscribe(async (newState) => {
|
||||
if (ctx.kv) {
|
||||
await ctx.kv.set(`command-state:${id}`, newState);
|
||||
}
|
||||
});
|
||||
ctx.state = reactiveState;
|
||||
return reactiveState;
|
||||
}
|
||||
|
||||
function instanceIdFromInteraction(interaction: ExecutableInteraction) {
|
||||
if (isAutocomplete(interaction)) {
|
||||
// autocomplete should not be stateful, they get no id
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isApplicationCommand(interaction)) {
|
||||
// for application commands, we create a new instance id
|
||||
const instance_id = crypto.randomUUID();
|
||||
return instance_id;
|
||||
}
|
||||
|
||||
const interact = interaction;
|
||||
const customId: string = interact.data.custom_id;
|
||||
const commandId = customId.split('_').pop();
|
||||
interaction;
|
||||
// command id should be a uuid
|
||||
if (commandId && /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(commandId)) {
|
||||
return commandId;
|
||||
}
|
||||
console.error(`Invalid command id extracted from interaction: ${customId}`);
|
||||
return '';
|
||||
}
|
||||
59
packages/lib/src/discord/commands/handle-commands.test.ts
Normal file
59
packages/lib/src/discord/commands/handle-commands.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { expect, test, mock, beforeEach, afterEach } from 'bun:test';
|
||||
import { handleCommands } from './handle-commands';
|
||||
import { CommandInteraction, Constants, ModalSubmitInteraction, type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||
import { CommandHandler } from '../types';
|
||||
|
||||
let commands: Record<string, CommandHandler<ApplicationCommandStructure>>;
|
||||
|
||||
beforeEach(() => {
|
||||
commands = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
commands = {};
|
||||
});
|
||||
|
||||
mock.module('./command-helpers', () => ({
|
||||
getCommandName: () => 'testCommand',
|
||||
}));
|
||||
|
||||
test('handleCommands executes command when interaction is CommandInteraction and command exists', async () => {
|
||||
const mockExecute = mock(() => Promise.resolve());
|
||||
const mockCommand = { definition: { name: 'testCommand' } as any, execute: mockExecute };
|
||||
commands['testCommand'] = mockCommand;
|
||||
|
||||
const mockInteraction = {
|
||||
type: Constants.InteractionTypes.APPLICATION_COMMAND,
|
||||
data: { name: 'testCommand' },
|
||||
} as any;
|
||||
Object.setPrototypeOf(mockInteraction, CommandInteraction.prototype);
|
||||
|
||||
handleCommands(mockInteraction, commands, {} as any);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledWith(mockInteraction, expect.any(Object));
|
||||
});
|
||||
|
||||
test('handleCommands executes command when interaction is CommandInteraction and command exists', async () => {
|
||||
const mockExecute = mock(() => Promise.resolve());
|
||||
const mockCommand = { definition: { name: 'testCommand' } as any, execute: mockExecute };
|
||||
commands['testCommand'] = mockCommand;
|
||||
|
||||
const mockInteraction = {
|
||||
type: Constants.InteractionTypes.MODAL_SUBMIT,
|
||||
data: { name: 'testCommand' },
|
||||
} as any;
|
||||
Object.setPrototypeOf(mockInteraction, ModalSubmitInteraction.prototype);
|
||||
|
||||
handleCommands(mockInteraction, commands, {} as any);
|
||||
|
||||
expect(mockExecute).toHaveBeenCalledWith(mockInteraction, expect.any(Object));
|
||||
});
|
||||
|
||||
test('handleCommands does nothing when interaction not a CommandInteraction, ModalSubmitInteraction, MessageComponentInteraction, or AutoCompleteInteraction', () => {
|
||||
const mockInteraction = {
|
||||
instanceof: (cls: any) => false,
|
||||
} as any;
|
||||
|
||||
// Should not throw or do anything
|
||||
expect(() => handleCommands(mockInteraction, commands, {} as any)).not.toThrow();
|
||||
});
|
||||
74
packages/lib/src/discord/commands/handle-commands.ts
Normal file
74
packages/lib/src/discord/commands/handle-commands.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { type ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||
import { augmentInteraction, getCommandName } from './command-helpers';
|
||||
import { injectInteraction } from './command-injection';
|
||||
import { getCommandState } from './command-state';
|
||||
import { type ExecutableInteraction } from '../types/interaction.type';
|
||||
import type { CommandHandler, PartialContext } from '../types';
|
||||
|
||||
export async function handleCommands(
|
||||
interaction: ExecutableInteraction,
|
||||
commands: Record<string, CommandHandler<ApplicationCommandStructure>>,
|
||||
ctx: PartialContext,
|
||||
) {
|
||||
ctx.state = await getCommandState(interaction, ctx);
|
||||
if (!ctx.state.name) {
|
||||
ctx.state.name = getCommandName(interaction);
|
||||
}
|
||||
|
||||
if (interaction.isAutocomplete() && ctx.state.name) {
|
||||
const acCommand = commands[ctx.state.name];
|
||||
return acCommand.execute(interaction, ctx as any);
|
||||
}
|
||||
|
||||
if (!ctx.state.id) {
|
||||
console.error(`No command ID found for interaction ${interaction.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const command = commands[ctx.state.name || ''];
|
||||
if (!command) {
|
||||
console.warn(`No command found for interaction: ${JSON.stringify(interaction, undefined, 2)}`);
|
||||
return;
|
||||
}
|
||||
cleanInteractionCustomIds(interaction, ctx.state.id);
|
||||
const [injectedInteraction, fullContext] = await injectInteraction(interaction, ctx);
|
||||
return command.execute(injectedInteraction, fullContext);
|
||||
}
|
||||
|
||||
export function initializeCommandHandling(commands: Record<string, CommandHandler<ApplicationCommandStructure>>, ctx: PartialContext) {
|
||||
ctx.client.on('interactionCreate', async (_interaction) => {
|
||||
const interaction = augmentInteraction(_interaction as any);
|
||||
if (interaction.isExecutable()) {
|
||||
handleCommands(interaction, commands, ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cleanInteractionCustomIds(interaction: ExecutableInteraction, id: string) {
|
||||
if ('components' in interaction && Array.isArray(interaction.components) && id) {
|
||||
removeCommandIdFromComponentCustomIds(interaction.components, id);
|
||||
}
|
||||
if ('data' in interaction && id) {
|
||||
if ('custom_id' in interaction.data && typeof interaction.data.custom_id === 'string') {
|
||||
interaction.data.custom_id = interaction.data.custom_id.replace(`_${id}`, '');
|
||||
}
|
||||
if ('components' in interaction.data && Array.isArray(interaction.data.components)) {
|
||||
removeCommandIdFromComponentCustomIds(interaction.data.components as any, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeCommandIdFromComponentCustomIds(components: { custom_id?: string; components?: any[] }[], commandId: string) {
|
||||
components.forEach((component) => {
|
||||
if ('custom_id' in component) {
|
||||
component.custom_id = component.custom_id.replace(`_${commandId}`, '');
|
||||
}
|
||||
if ('components' in component && Array.isArray(component.components)) {
|
||||
removeCommandIdFromComponentCustomIds(component.components, commandId);
|
||||
}
|
||||
|
||||
if ('component' in component && 'custom_id' in (component as any).component && Array.isArray(component.components)) {
|
||||
(component.component as any).custom_id = (component.component as any).custom_id.replace(`_${commandId}`, '');
|
||||
}
|
||||
});
|
||||
}
|
||||
19
packages/lib/src/discord/commands/import-commands.test.ts
Normal file
19
packages/lib/src/discord/commands/import-commands.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { expect, test, mock } from 'bun:test';
|
||||
import { importCommands } from './import-commands';
|
||||
import path from 'node:path';
|
||||
|
||||
test('importCommands imports commands from files matching pattern', async () => {
|
||||
const commands = await importCommands('**/*.command.{js,ts}', path.join(__dirname, '../../fixtures'));
|
||||
|
||||
expect(commands).toHaveProperty('test1');
|
||||
expect(commands).toHaveProperty('test2');
|
||||
expect(commands.test1.definition.name).toBe('test1');
|
||||
expect(commands.test2.definition.name).toBe('test2');
|
||||
});
|
||||
|
||||
test('importCommands uses default pattern and baseDir', async () => {
|
||||
const commands = await importCommands();
|
||||
|
||||
// Since there are no command files in src, it should be empty
|
||||
expect(commands).toEqual({});
|
||||
});
|
||||
19
packages/lib/src/discord/commands/import-commands.ts
Normal file
19
packages/lib/src/discord/commands/import-commands.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Glob } from 'bun';
|
||||
import { join } from 'node:path';
|
||||
import type { ApplicationCommandStructure } from '@projectdysnomia/dysnomia';
|
||||
import type { CommandHandler } from '../types';
|
||||
|
||||
export async function importCommands(
|
||||
pattern: string = '**/*.command.{js,ts,jsx,tsx}',
|
||||
baseDir: string = join(process.cwd(), 'src'),
|
||||
commandRegistry: Record<string, CommandHandler<ApplicationCommandStructure>> = {},
|
||||
): Promise<Record<string, CommandHandler<ApplicationCommandStructure>>> {
|
||||
const glob = new Glob(pattern);
|
||||
|
||||
for await (const file of glob.scan({ cwd: baseDir, absolute: true })) {
|
||||
const command = (await import(file)).default as CommandHandler<ApplicationCommandStructure>;
|
||||
commandRegistry[command.definition.name] = command;
|
||||
}
|
||||
|
||||
return commandRegistry;
|
||||
}
|
||||
7
packages/lib/src/discord/commands/index.ts
Normal file
7
packages/lib/src/discord/commands/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './command-handler';
|
||||
export * from './import-commands';
|
||||
export * from './handle-commands';
|
||||
export * from './command-helpers';
|
||||
export * from './register-commands';
|
||||
export * from './command-state';
|
||||
export * from './option-builders';
|
||||
80
packages/lib/src/discord/commands/option-builders.ts
Normal file
80
packages/lib/src/discord/commands/option-builders.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
Constants,
|
||||
type ApplicationCommandOptions,
|
||||
type ApplicationCommandOptionsBoolean,
|
||||
type ApplicationCommandOptionsInteger,
|
||||
type ApplicationCommandOptionsMentionable,
|
||||
type ApplicationCommandOptionsNumber,
|
||||
type ApplicationCommandOptionsRole,
|
||||
type ApplicationCommandOptionsString,
|
||||
type ApplicationCommandOptionsSubCommand,
|
||||
type ApplicationCommandOptionsSubCommandGroup,
|
||||
type ApplicationCommandOptionsUser,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
export type StringOptionDefinition = Omit<ApplicationCommandOptionsString, 'type'> & { autocomplete?: boolean };
|
||||
export function stringOption(options: StringOptionDefinition): ApplicationCommandOptionsString {
|
||||
const def = options as ApplicationCommandOptionsString;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.STRING;
|
||||
return def;
|
||||
}
|
||||
export type IntegerOptionDefinition = Omit<ApplicationCommandOptionsInteger, 'type'> & { autocomplete?: boolean };
|
||||
export function integerOption(options: IntegerOptionDefinition): ApplicationCommandOptionsInteger {
|
||||
const def = options as ApplicationCommandOptionsInteger;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.INTEGER;
|
||||
return def;
|
||||
}
|
||||
export type BooleanOptionDefinition = Omit<ApplicationCommandOptionsBoolean, 'type'>;
|
||||
export function booleanOption(options: BooleanOptionDefinition): ApplicationCommandOptionsBoolean {
|
||||
const def = options as ApplicationCommandOptionsBoolean;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.BOOLEAN;
|
||||
return def;
|
||||
}
|
||||
export type UserOptionDefinition = Omit<ApplicationCommandOptionsUser, 'type'> & { autocomplete?: boolean };
|
||||
export function userOption(options: UserOptionDefinition): ApplicationCommandOptionsUser {
|
||||
const def = options as ApplicationCommandOptionsUser;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.USER;
|
||||
return def;
|
||||
}
|
||||
export type ChannelOptionDefinition = Omit<ApplicationCommandOptions, 'type'> & { autocomplete?: boolean };
|
||||
export function channelOption(options: ChannelOptionDefinition): ApplicationCommandOptions {
|
||||
const def = options as ApplicationCommandOptions;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.CHANNEL;
|
||||
return def;
|
||||
}
|
||||
export type RoleOptionDefinition = Omit<ApplicationCommandOptionsRole, 'type'> & { autocomplete?: boolean };
|
||||
export function roleOption(options: RoleOptionDefinition): ApplicationCommandOptionsRole {
|
||||
const def = options as ApplicationCommandOptionsRole;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.ROLE;
|
||||
return def;
|
||||
}
|
||||
export type MentionableOptionDefinition = Omit<ApplicationCommandOptionsMentionable, 'type'>;
|
||||
export function mentionableOption(options: MentionableOptionDefinition): ApplicationCommandOptionsMentionable {
|
||||
const def = options as ApplicationCommandOptionsMentionable;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.MENTIONABLE;
|
||||
return def;
|
||||
}
|
||||
export type NumberOptionDefinition = Omit<ApplicationCommandOptionsNumber, 'type'> & { autocomplete?: boolean };
|
||||
export function numberOption(options: NumberOptionDefinition): ApplicationCommandOptionsNumber {
|
||||
const def = options as ApplicationCommandOptionsNumber;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.NUMBER;
|
||||
return def;
|
||||
}
|
||||
export type AttachmentOptionDefinition = Omit<ApplicationCommandOptions, 'type'>;
|
||||
export function attachmentOption(options: AttachmentOptionDefinition): ApplicationCommandOptions {
|
||||
const def = options as ApplicationCommandOptions;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.ATTACHMENT;
|
||||
return def;
|
||||
}
|
||||
export type SubCommandOptionDefinition = Omit<ApplicationCommandOptionsSubCommand, 'type'>;
|
||||
export function subCommandOption(options: SubCommandOptionDefinition): ApplicationCommandOptionsSubCommand {
|
||||
const def = options as ApplicationCommandOptionsSubCommand;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND;
|
||||
return def;
|
||||
}
|
||||
export type SubCommandGroupOptionDefinition = Omit<ApplicationCommandOptionsSubCommandGroup, 'type'>;
|
||||
export function subCommandGroupOption(options: SubCommandGroupOptionDefinition): ApplicationCommandOptionsSubCommandGroup {
|
||||
const def = options as ApplicationCommandOptionsSubCommandGroup;
|
||||
def.type = Constants.ApplicationCommandOptionTypes.SUB_COMMAND_GROUP;
|
||||
return def;
|
||||
}
|
||||
11
packages/lib/src/discord/commands/register-commands.ts
Normal file
11
packages/lib/src/discord/commands/register-commands.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ApplicationCommandStructure, Client } from '@projectdysnomia/dysnomia';
|
||||
|
||||
export async function registerCommands(client: Client, commands: ApplicationCommandStructure[]) {
|
||||
if (!client) throw new Error('Client not initialized');
|
||||
if (!(await client.getCommands()).length || process.env.RESET_COMMANDS === 'true' || process.env.NODE_ENV === 'development') {
|
||||
console.debug('Registering commands...');
|
||||
const response = await client.bulkEditCommands(commands);
|
||||
console.debug(`Registered ${response.length} commands.`);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
339
packages/lib/src/discord/components/builders.ts
Normal file
339
packages/lib/src/discord/components/builders.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import {
|
||||
Constants,
|
||||
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,
|
||||
type ModalSubmitInteractionData,
|
||||
type FileUploadComponent,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
export type ActionRowItem = Button | StringSelectMenu | UserSelectMenu | RoleSelectMenu | MentionableSelectMenu | ChannelSelectMenu;
|
||||
export const actionRow = (...components: ActionRowItem[]): ActionRow => ({
|
||||
type: Constants.ComponentTypes.ACTION_ROW,
|
||||
components: components.filter((c) => c),
|
||||
});
|
||||
|
||||
export enum ButtonStyle {
|
||||
PRIMARY = 1,
|
||||
SECONDARY = 2,
|
||||
SUCCESS = 3,
|
||||
DANGER = 4,
|
||||
}
|
||||
|
||||
export interface ButtonOptions {
|
||||
style?: ButtonStyle;
|
||||
emoji?: PartialEmoji;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const button = (label: string, custom_id: string, options?: ButtonOptions): InteractionButton => ({
|
||||
type: Constants.ComponentTypes.BUTTON,
|
||||
style: options?.style ?? Constants.ButtonStyles.PRIMARY,
|
||||
label,
|
||||
custom_id,
|
||||
...options,
|
||||
});
|
||||
|
||||
export interface URLButtonOptions {
|
||||
emoji?: PartialEmoji;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const urlButton = (label: string, url: string, options?: URLButtonOptions): URLButton => ({
|
||||
type: Constants.ComponentTypes.BUTTON,
|
||||
style: Constants.ButtonStyles.LINK,
|
||||
label,
|
||||
url,
|
||||
...options,
|
||||
});
|
||||
|
||||
export interface PremiumButtonOptions {
|
||||
emoji?: PartialEmoji;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const premiumButton = (sku_id: string, options?: PremiumButtonOptions): PremiumButton => ({
|
||||
type: Constants.ComponentTypes.BUTTON,
|
||||
style: Constants.ButtonStyles.PREMIUM,
|
||||
sku_id,
|
||||
...options,
|
||||
});
|
||||
|
||||
export interface StringSelectOpts {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
required?: boolean; // Note: not actually a property of StringSelectMenu, but useful for modals
|
||||
}
|
||||
|
||||
export interface StringSelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
emoji?: PartialEmoji;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export interface StringSelectOptions {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const stringSelect = (custom_id: string, selectOpts: StringSelectOpts, ...options: StringSelectOption[]): StringSelectMenu => ({
|
||||
type: Constants.ComponentTypes.STRING_SELECT,
|
||||
custom_id,
|
||||
options,
|
||||
placeholder: selectOpts.placeholder,
|
||||
min_values: selectOpts.min_values ?? 1,
|
||||
max_values: selectOpts.max_values ?? 1,
|
||||
disabled: selectOpts.disabled ?? false,
|
||||
required: selectOpts.required ?? false, // Note: not actually a property of StringSelectMenu, but useful for modals
|
||||
});
|
||||
|
||||
export interface InputOptions {
|
||||
isParagraph?: boolean;
|
||||
label?: string;
|
||||
min_length?: number;
|
||||
max_length?: number;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const input = (custom_id: string, options?: InputOptions): TextInput => ({
|
||||
type: Constants.ComponentTypes.TEXT_INPUT,
|
||||
custom_id,
|
||||
style: options.isParagraph ? Constants.TextInputStyles.PARAGRAPH : Constants.TextInputStyles.SHORT,
|
||||
label: options?.label,
|
||||
min_length: options?.min_length ?? 0,
|
||||
max_length: options?.max_length ?? 4000,
|
||||
required: options?.required ?? false,
|
||||
value: options?.value,
|
||||
placeholder: options?.placeholder,
|
||||
});
|
||||
|
||||
export interface UserSelectOptions {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
required?: boolean; // if on a modal
|
||||
default_values?: Array<{ id: string; type: 'user' }>;
|
||||
}
|
||||
|
||||
export const userSelect = (custom_id: string, options?: UserSelectOptions): UserSelectMenu => ({
|
||||
type: Constants.ComponentTypes.USER_SELECT,
|
||||
custom_id,
|
||||
placeholder: options?.placeholder ?? '',
|
||||
min_values: options?.min_values ?? 1,
|
||||
max_values: options?.max_values ?? 1,
|
||||
disabled: options?.disabled ?? false,
|
||||
default_values: options?.default_values ?? [],
|
||||
});
|
||||
|
||||
export interface RoleSelectOptions {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
required?: boolean; // if on a modal
|
||||
default_values?: Array<{ id: string; type: 'role' }>;
|
||||
}
|
||||
|
||||
export const roleSelect = (custom_id: string, options?: RoleSelectOptions): RoleSelectMenu => ({
|
||||
type: Constants.ComponentTypes.ROLE_SELECT,
|
||||
custom_id,
|
||||
placeholder: options?.placeholder ?? '',
|
||||
min_values: options?.min_values ?? 1,
|
||||
max_values: options?.max_values ?? 1,
|
||||
disabled: options?.disabled ?? false,
|
||||
default_values: options?.default_values ?? [],
|
||||
});
|
||||
|
||||
export interface MentionableSelectOptions {
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
required?: boolean; // if on a modal
|
||||
default_values?: Array<{ id: string; type: 'user' | 'role' }>;
|
||||
}
|
||||
|
||||
export const mentionableSelect = (custom_id: string, options?: MentionableSelectOptions): MentionableSelectMenu => ({
|
||||
type: Constants.ComponentTypes.MENTIONABLE_SELECT,
|
||||
custom_id,
|
||||
placeholder: options?.placeholder ?? '',
|
||||
min_values: options?.min_values ?? 1,
|
||||
max_values: options?.max_values ?? 1,
|
||||
disabled: options?.disabled ?? false,
|
||||
default_values: options?.default_values ?? [],
|
||||
});
|
||||
|
||||
export interface ChannelSelectOptions {
|
||||
channel_types?: GuildChannelTypes[];
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
disabled?: boolean;
|
||||
required?: boolean; // if on a modal
|
||||
default_values?: Array<{ id: string; type: 'channel' }>;
|
||||
}
|
||||
|
||||
export const channelSelect = (custom_id: string, options?: ChannelSelectOptions): ChannelSelectMenu => ({
|
||||
type: Constants.ComponentTypes.CHANNEL_SELECT,
|
||||
custom_id,
|
||||
channel_types: options?.channel_types ?? [],
|
||||
placeholder: options?.placeholder ?? '',
|
||||
min_values: options?.min_values ?? 1,
|
||||
max_values: options?.max_values ?? 1,
|
||||
disabled: options?.disabled ?? false,
|
||||
default_values: options?.default_values ?? [],
|
||||
});
|
||||
|
||||
export interface SectionOptions {
|
||||
components: Array<TextDisplayComponent>;
|
||||
accessory: Button | ThumbnailComponent;
|
||||
}
|
||||
|
||||
export const section = (accessory: Button | ThumbnailComponent, ...components: Array<TextDisplayComponent>): SectionComponent => ({
|
||||
type: Constants.ComponentTypes.SECTION,
|
||||
accessory,
|
||||
components: components.filter((c) => c),
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a text display component where the text will be displayed similar to a message: supports markdown
|
||||
* @param content The text content to display.
|
||||
* @returns The created text display component.
|
||||
*/
|
||||
export const text = (content: string) => ({
|
||||
type: Constants.ComponentTypes.TEXT_DISPLAY,
|
||||
content,
|
||||
});
|
||||
|
||||
export interface ThumbnailOptions {
|
||||
media: {
|
||||
url: string; // Supports arbitrary urls and attachment://<filename> references
|
||||
};
|
||||
description?: string;
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
export const thumbnail = (url: string, description?: string, spoiler?: boolean): ThumbnailComponent => ({
|
||||
type: Constants.ComponentTypes.THUMBNAIL,
|
||||
media: {
|
||||
url,
|
||||
},
|
||||
description,
|
||||
spoiler,
|
||||
});
|
||||
|
||||
export interface MediaItem {
|
||||
url: string; // Supports arbitrary urls and attachment://<filename> references
|
||||
description?: string;
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
export const gallery = (...items: MediaItem[]): MediaGalleryComponent => ({
|
||||
type: Constants.ComponentTypes.MEDIA_GALLERY,
|
||||
items: items.map((item) => ({
|
||||
type: Constants.ComponentTypes.FILE,
|
||||
media: { url: item.url },
|
||||
description: item.description,
|
||||
spoiler: item.spoiler,
|
||||
})),
|
||||
});
|
||||
|
||||
export interface FileOptions {
|
||||
url: string; // Supports only attachment://<filename> references
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
export const file = (url: string, spoiler?: boolean): FileComponent => ({
|
||||
type: Constants.ComponentTypes.FILE,
|
||||
file: {
|
||||
url,
|
||||
},
|
||||
spoiler,
|
||||
});
|
||||
|
||||
export enum Padding {
|
||||
SMALL = 1,
|
||||
LARGE = 2,
|
||||
}
|
||||
|
||||
export interface SeparatorOptions {
|
||||
divider?: boolean;
|
||||
spacing?: Padding;
|
||||
}
|
||||
export const separator = (spacing?: Padding, divider?: boolean): SeparatorComponent => ({
|
||||
type: Constants.ComponentTypes.SEPARATOR,
|
||||
divider,
|
||||
spacing: spacing ?? Padding.SMALL,
|
||||
});
|
||||
|
||||
export interface ContainerOptions {
|
||||
accent_color?: number;
|
||||
spoiler?: boolean;
|
||||
}
|
||||
|
||||
export type ContainerItems =
|
||||
| ActionRow
|
||||
| TextDisplayComponent
|
||||
| SectionComponent
|
||||
| MediaGalleryComponent
|
||||
| SeparatorComponent
|
||||
| FileComponent;
|
||||
|
||||
export const container = (options: ContainerOptions, ...components: ContainerItems[]): ContainerComponent => ({
|
||||
type: Constants.ComponentTypes.CONTAINER,
|
||||
...options,
|
||||
components: components.filter((c) => c),
|
||||
});
|
||||
|
||||
// Modals
|
||||
|
||||
export interface LabelOptions {
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const label = (options: LabelOptions, component: LabelComponent['component']): LabelComponent => ({
|
||||
type: Constants.ComponentTypes.LABEL,
|
||||
label: options.label,
|
||||
description: options.description,
|
||||
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: components.filter((c) => c),
|
||||
} as any);
|
||||
23
packages/lib/src/discord/components/helpers.ts
Normal file
23
packages/lib/src/discord/components/helpers.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
Constants,
|
||||
type ComponentBase,
|
||||
type ModalSubmitInteractionDataLabelComponent,
|
||||
type ModalSubmitInteractionDataSelectComponent,
|
||||
type ModalSubmitInteractionDataTextInputComponent,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
|
||||
export function isModalLabel(component: ComponentBase): component is ModalSubmitInteractionDataLabelComponent {
|
||||
return component.type === Constants.ComponentTypes.LABEL;
|
||||
}
|
||||
|
||||
export function isModalTextInput(component: ComponentBase): component is ModalSubmitInteractionDataTextInputComponent {
|
||||
return component.type === Constants.ComponentTypes.TEXT_INPUT;
|
||||
}
|
||||
|
||||
export function isModalSelect(component: ComponentBase): component is ModalSubmitInteractionDataSelectComponent {
|
||||
return component.type === Constants.ComponentTypes.STRING_SELECT;
|
||||
}
|
||||
|
||||
export function componentHasIdPrefix(component: ComponentBase, prefix: string): boolean {
|
||||
return (isModalTextInput(component) || isModalSelect(component)) && component.custom_id.startsWith(prefix);
|
||||
}
|
||||
2
packages/lib/src/discord/components/index.ts
Normal file
2
packages/lib/src/discord/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './helpers';
|
||||
export * from './builders';
|
||||
2
packages/lib/src/discord/constants/index.ts
Normal file
2
packages/lib/src/discord/constants/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './text';
|
||||
export * from './locale';
|
||||
34
packages/lib/src/discord/constants/locale.ts
Normal file
34
packages/lib/src/discord/constants/locale.ts
Normal 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',
|
||||
}
|
||||
2
packages/lib/src/discord/constants/text.ts
Normal file
2
packages/lib/src/discord/constants/text.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const WHITE_SPACE = ' '; // non-breaking space
|
||||
export const BREAKING_WHITE_SPACE = '\u200B';
|
||||
54
packages/lib/src/discord/core/bot.ts
Normal file
54
packages/lib/src/discord/core/bot.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { importCommands, initializeCommandHandling, registerCommands } from '../commands';
|
||||
import { Client } from '@projectdysnomia/dysnomia';
|
||||
import kv, { asyncKV } from '@/util/kv.js';
|
||||
import type { KVStore } from './kv-store.type.ts.ts';
|
||||
import type { Cache } from './cache.type.ts';
|
||||
|
||||
export interface DiscordBotOptions {
|
||||
token?: string;
|
||||
intents?: number[];
|
||||
commandPattern?: string;
|
||||
commandBaseDir?: string;
|
||||
keyStore?: KVStore;
|
||||
cache?: Cache;
|
||||
onError?: (error: Error) => void;
|
||||
onReady?: () => void;
|
||||
}
|
||||
|
||||
export function startBot({
|
||||
token = process.env.DISCORD_BOT_TOKEN || '',
|
||||
intents = [],
|
||||
commandPattern = '**/*.command.{js,ts,jsx,tsx}',
|
||||
commandBaseDir = 'src',
|
||||
keyStore = asyncKV,
|
||||
cache = kv,
|
||||
onError,
|
||||
onReady,
|
||||
}: DiscordBotOptions = {}): Client {
|
||||
const client = new Client(`Bot ${token}`, {
|
||||
gateway: {
|
||||
intents,
|
||||
},
|
||||
});
|
||||
|
||||
client.on('ready', async () => {
|
||||
console.debug(`Logged in as ${client.user?.username}#${client.user?.discriminator}`);
|
||||
onReady?.();
|
||||
const commands = await importCommands(commandPattern, commandBaseDir);
|
||||
await registerCommands(
|
||||
client,
|
||||
Object.values(commands).map((cmd) => cmd.definition),
|
||||
);
|
||||
initializeCommandHandling(commands, { client, cache, kv: keyStore });
|
||||
console.debug('Bot is ready and command handling is initialized.');
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
console.error('An error occurred:', error);
|
||||
onError?.(error);
|
||||
});
|
||||
|
||||
client.connect().catch(console.error);
|
||||
|
||||
return client;
|
||||
}
|
||||
6
packages/lib/src/discord/core/cache.type.ts
Normal file
6
packages/lib/src/discord/core/cache.type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Cache {
|
||||
get: <T>(key: string) => T | undefined;
|
||||
set: <T>(key: string, value: T, ttl?: number | string) => boolean;
|
||||
del: (key: string | string[]) => number;
|
||||
has: (key: string) => boolean;
|
||||
}
|
||||
3
packages/lib/src/discord/core/index.ts
Normal file
3
packages/lib/src/discord/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './bot.ts';
|
||||
export * from './cache.type.ts';
|
||||
export * from './kv-store.type.ts.ts';
|
||||
7
packages/lib/src/discord/core/kv-store.type.ts.ts
Normal file
7
packages/lib/src/discord/core/kv-store.type.ts.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface KVStore {
|
||||
get: <T>(key: string) => Promise<T | undefined>;
|
||||
set: (key: string, value: any) => Promise<boolean>;
|
||||
delete: (key: string) => Promise<number>;
|
||||
has: (key: string) => Promise<boolean>;
|
||||
clear: () => Promise<void>;
|
||||
}
|
||||
7
packages/lib/src/discord/index.ts
Normal file
7
packages/lib/src/discord/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './constants';
|
||||
export * from './commands';
|
||||
export * from './core';
|
||||
export * from './jsx';
|
||||
export * from './components';
|
||||
export * from './pages';
|
||||
export * from './types';
|
||||
7
packages/lib/src/discord/jsd/createElement.ts
Normal file
7
packages/lib/src/discord/jsd/createElement.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function createElement(tag: string, attrs: Record<string, any> = {}, ...children: any[]) {
|
||||
return {
|
||||
tag,
|
||||
attrs,
|
||||
children,
|
||||
};
|
||||
}
|
||||
2
packages/lib/src/discord/jsd/index.ts
Normal file
2
packages/lib/src/discord/jsd/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './parser';
|
||||
export * from './createElement';
|
||||
10
packages/lib/src/discord/jsd/parser.test.ts
Normal file
10
packages/lib/src/discord/jsd/parser.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { parseJSDFile } from './parser_new';
|
||||
import path from 'node:path';
|
||||
|
||||
describe('parseJSDFile', () => {
|
||||
it('should parse a JSD file', async () => {
|
||||
const result = await parseJSDFile(path.join(__dirname, '../../fixtures/jsd/test.tsd'));
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
});
|
||||
97
packages/lib/src/discord/jsd/parser.ts
Normal file
97
packages/lib/src/discord/jsd/parser.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import parse, { type DOMNode } from 'html-dom-parser';
|
||||
import type { ChildNode } from 'domhandler';
|
||||
|
||||
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
|
||||
|
||||
export async function parseJSDFile(filename: string) {
|
||||
const content = (await fs.readFile(filename)).toString();
|
||||
|
||||
const matches = JSD_STRING.exec(content);
|
||||
if (matches) {
|
||||
let html = matches[1] + '>';
|
||||
const root = parse(html);
|
||||
const translated = translate(root[0]);
|
||||
const str = content.replace(matches[1] + '>', translated);
|
||||
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
interface state {
|
||||
inInterpolation?: boolean;
|
||||
children?: string[][];
|
||||
parent?: Text[];
|
||||
}
|
||||
|
||||
function translate(root: DOMNode | ChildNode | null, state: state = {}): string | null {
|
||||
if (!root || typeof root !== 'object') return null;
|
||||
|
||||
let children = [];
|
||||
if ('children' in root && Array.isArray(root.children) && root.children.length > 0) {
|
||||
for (const child of root.children) {
|
||||
const translated = translate(child, state);
|
||||
if (translated) {
|
||||
if (state.inInterpolation && state.parent[state.children.length - 1] === child) {
|
||||
state.children[state.children.length - 1].push(translated);
|
||||
} else {
|
||||
children.push(translated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ('nodeType' in root && root.nodeType === 3) {
|
||||
if (root.data.trim() === '') return null;
|
||||
return parseText(root.data.trim(), state, root);
|
||||
}
|
||||
|
||||
if ('name' in root && root.name) {
|
||||
let tagName = root.name || 'unknown';
|
||||
let attrs = 'attribs' in root ? root.attribs : {};
|
||||
return `StarKitten.createElement("${tagName}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
|
||||
}
|
||||
}
|
||||
|
||||
const JSD_INTERPOLATION = /\{(.+)\}/gs;
|
||||
const JSD_START_EXP_INTERPOLATION = /\{(.+)\(/gs;
|
||||
const JSD_END_EXP_INTERPOLATION = /\)(.+)\}/gs;
|
||||
|
||||
function parseText(text: string, state: state = {}, parent: Text = {} as any): string {
|
||||
let interpolations = text.match(JSD_INTERPOLATION);
|
||||
if (!interpolations) {
|
||||
if (text.match(JSD_START_EXP_INTERPOLATION)) {
|
||||
state.inInterpolation = true;
|
||||
state.children = state.children || [[]];
|
||||
state.parent = state.parent || [];
|
||||
state.parent.push(parent);
|
||||
return text.substring(1, text.length - 1);
|
||||
} else if (text.match(JSD_END_EXP_INTERPOLATION)) {
|
||||
const combined = state.children?.[state.children.length - 1].join(' ');
|
||||
state.children?.[state.children.length - 1].splice(0);
|
||||
state.children?.pop();
|
||||
state.parent?.pop();
|
||||
if (state.children.length === 0) {
|
||||
state.inInterpolation = false;
|
||||
return combined + ' ' + text.substring(1, text.length - 1);
|
||||
}
|
||||
}
|
||||
return `"${text}"`;
|
||||
} else {
|
||||
text = replaceInterpolations(text);
|
||||
return `"${text}"`;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceInterpolations(text: string, isOnJSON: boolean = false) {
|
||||
let interpolations = null;
|
||||
|
||||
while ((interpolations = JSD_INTERPOLATION.exec(text))) {
|
||||
if (isOnJSON) {
|
||||
text = text.replace(`"{${interpolations[1]}}"`, interpolations[1]);
|
||||
} else {
|
||||
text = text.replace(`{${interpolations[1]}}`, `"+ ${interpolations[1]} +"`);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
101
packages/lib/src/discord/jsd/parser_new.ts
Normal file
101
packages/lib/src/discord/jsd/parser_new.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import * as acorn from 'acorn';
|
||||
import jsx from 'acorn-jsx';
|
||||
|
||||
const JSD_STRING = /\(\s*(<.*)>\s*\)/gs;
|
||||
|
||||
const parser = acorn.Parser.extend(jsx());
|
||||
|
||||
export async function parseJSDFile(filename: string) {
|
||||
const content = (await fs.readFile(filename)).toString();
|
||||
|
||||
const matches = JSD_STRING.exec(content);
|
||||
if (matches) {
|
||||
const jsxc = matches[1] + '>';
|
||||
const ast = parser.parse(jsxc, { ecmaVersion: 2020, sourceType: 'module' });
|
||||
const translated = traverseJSX((ast.body[0] as any).expression);
|
||||
const str = content.replace(matches[1] + '>', translated);
|
||||
await fs.writeFile(filename.replace('.tsd', '.ts'), str);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function traverseJSX(node: any): string {
|
||||
if (node.type === 'JSXElement') {
|
||||
const tag = node.openingElement.name.name;
|
||||
const attrs: Record<string, any> = {};
|
||||
for (const attr of node.openingElement.attributes) {
|
||||
if (attr.type === 'JSXAttribute') {
|
||||
const name = attr.name.name;
|
||||
const value = attr.value;
|
||||
if (value.type === 'Literal') {
|
||||
attrs[name] = value.value;
|
||||
} else if (value.type === 'JSXExpressionContainer') {
|
||||
attrs[name] = `{${generateCode(value.expression)}}`;
|
||||
} else if (value) {
|
||||
attrs[name] = value.raw;
|
||||
}
|
||||
}
|
||||
}
|
||||
const children = [];
|
||||
for (const child of node.children) {
|
||||
const translated = traverseJSX(child);
|
||||
if (translated) {
|
||||
children.push(translated);
|
||||
}
|
||||
}
|
||||
return `StarKitten.createElement("${tag}", ${JSON.stringify(attrs)}${children.length > 0 ? ', ' + children.join(', ') : ''})`;
|
||||
} else if (node.type === 'JSXExpressionContainer') {
|
||||
const expr = generateCode(node.expression);
|
||||
if (node.expression.type === 'TemplateLiteral' || (node.expression.type === 'Literal' && typeof node.expression.value === 'string')) {
|
||||
return `""+ ${expr} +""`;
|
||||
} else {
|
||||
return expr;
|
||||
}
|
||||
} else if (node.type === 'JSXText') {
|
||||
const text = node.value.trim();
|
||||
if (text) {
|
||||
return `"${text}"`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function generateCode(node: any): string {
|
||||
if (node.type === 'JSXElement') {
|
||||
return traverseJSX(node);
|
||||
} else if (node.type === 'Identifier') {
|
||||
return node.name;
|
||||
} else if (node.type === 'Literal') {
|
||||
return JSON.stringify(node.value);
|
||||
} else if (node.type === 'TemplateLiteral') {
|
||||
const quasis = node.quasis.map((q: any) => q.value.raw);
|
||||
const expressions = node.expressions.map((e: any) => generateCode(e));
|
||||
let result = quasis[0];
|
||||
for (let i = 0; i < expressions.length; i++) {
|
||||
result += '${' + expressions[i] + '}' + quasis[i + 1];
|
||||
}
|
||||
return '`' + result + '`';
|
||||
} else if (node.type === 'MemberExpression') {
|
||||
const op = node.optional ? '?.' : '.';
|
||||
return generateCode(node.object) + op + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
|
||||
} else if (node.type === 'OptionalMemberExpression') {
|
||||
return generateCode(node.object) + '?.' + (node.computed ? '[' + generateCode(node.property) + ']' : generateCode(node.property));
|
||||
} else if (node.type === 'CallExpression') {
|
||||
return generateCode(node.callee) + '(' + node.arguments.map((a: any) => generateCode(a)).join(', ') + ')';
|
||||
} else if (node.type === 'BinaryExpression') {
|
||||
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
|
||||
} else if (node.type === 'ConditionalExpression') {
|
||||
return generateCode(node.test) + ' ? ' + generateCode(node.consequent) + ' : ' + generateCode(node.alternate);
|
||||
} else if (node.type === 'LogicalExpression') {
|
||||
return generateCode(node.left) + ' ' + node.operator + ' ' + generateCode(node.right);
|
||||
} else if (node.type === 'UnaryExpression') {
|
||||
return node.operator + generateCode(node.argument);
|
||||
} else if (node.type === 'ObjectExpression') {
|
||||
return '{' + node.properties.map((p: any) => generateCode(p.key) + ': ' + generateCode(p.value)).join(', ') + '}';
|
||||
} else if (node.type === 'ArrayExpression') {
|
||||
return '[' + node.elements.map((e: any) => generateCode(e)).join(', ') + ']';
|
||||
} else {
|
||||
return node.raw || node.name || 'unknown';
|
||||
}
|
||||
}
|
||||
6
packages/lib/src/discord/jsx/components/action-row.ts
Normal file
6
packages/lib/src/discord/jsx/components/action-row.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { actionRow } from '@/discord/components';
|
||||
import type { ActionRowElement } from './element.types';
|
||||
|
||||
export function ActionRow(props: { children: ActionRowElement['children'] }) {
|
||||
return actionRow(...(Array.isArray(props.children) ? props.children : [props.children]));
|
||||
}
|
||||
14
packages/lib/src/discord/jsx/components/button.ts
Normal file
14
packages/lib/src/discord/jsx/components/button.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { button, premiumButton, urlButton } from '@/discord/components';
|
||||
import type { ButtonElement, PremiumButtonElement, URLButtonElement } from './element.types';
|
||||
|
||||
export function Button(props: ButtonElement['props']) {
|
||||
return button(props.label, props.customId, { style: props.style, emoji: props.emoji, disabled: props.disabled });
|
||||
}
|
||||
|
||||
export function URLButton(props: URLButtonElement['props']) {
|
||||
return urlButton(props.label, props.url, { emoji: props.emoji, disabled: props.disabled });
|
||||
}
|
||||
|
||||
export function PremiumButton(props: PremiumButtonElement['props']) {
|
||||
return premiumButton(props.skuId, { emoji: props.emoji, disabled: props.disabled });
|
||||
}
|
||||
14
packages/lib/src/discord/jsx/components/channel-select.ts
Normal file
14
packages/lib/src/discord/jsx/components/channel-select.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
9
packages/lib/src/discord/jsx/components/container.ts
Normal file
9
packages/lib/src/discord/jsx/components/container.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { container } from '@/discord/components';
|
||||
import type { ContainerElement } from './element.types';
|
||||
|
||||
export function Container(props: ContainerElement['props'] & { children: ContainerElement['children'] }) {
|
||||
return container(
|
||||
{ accent_color: props.accent, spoiler: props.spoiler },
|
||||
...(Array.isArray(props.children) ? props.children : [props.children]),
|
||||
);
|
||||
}
|
||||
235
packages/lib/src/discord/jsx/components/element.types.ts
Normal file
235
packages/lib/src/discord/jsx/components/element.types.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
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;
|
||||
description?: 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 TextElement {
|
||||
type: 'text';
|
||||
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;
|
||||
}
|
||||
6
packages/lib/src/discord/jsx/components/file.ts
Normal file
6
packages/lib/src/discord/jsx/components/file.ts
Normal 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);
|
||||
}
|
||||
7
packages/lib/src/discord/jsx/components/gallery.ts
Normal file
7
packages/lib/src/discord/jsx/components/gallery.ts
Normal 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);
|
||||
}
|
||||
19
packages/lib/src/discord/jsx/components/index.ts
Normal file
19
packages/lib/src/discord/jsx/components/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export * from './action-row';
|
||||
export * from './button';
|
||||
export * from './channel-select';
|
||||
export * from './container';
|
||||
export * from './file';
|
||||
export * from './gallery';
|
||||
export * from './label';
|
||||
export * from './media';
|
||||
export * from './mentionable-select';
|
||||
export * from './modal';
|
||||
export * from './option';
|
||||
export * from './role-select';
|
||||
export * from './section';
|
||||
export * from './separator';
|
||||
export * from './string-select';
|
||||
export * from './text';
|
||||
export * from './text-input';
|
||||
export * from './thumbnail';
|
||||
export * from './user-select';
|
||||
6
packages/lib/src/discord/jsx/components/label.ts
Normal file
6
packages/lib/src/discord/jsx/components/label.ts
Normal 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, props.children);
|
||||
}
|
||||
5
packages/lib/src/discord/jsx/components/media.ts
Normal file
5
packages/lib/src/discord/jsx/components/media.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { MediaElement } from './element.types';
|
||||
|
||||
export function Media(props: MediaElement['props']) {
|
||||
return props;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
6
packages/lib/src/discord/jsx/components/modal.ts
Normal file
6
packages/lib/src/discord/jsx/components/modal.ts
Normal 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]));
|
||||
}
|
||||
5
packages/lib/src/discord/jsx/components/option.ts
Normal file
5
packages/lib/src/discord/jsx/components/option.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { OptionElement } from './element.types';
|
||||
|
||||
export function Option(props: OptionElement['props']) {
|
||||
return props;
|
||||
}
|
||||
13
packages/lib/src/discord/jsx/components/role-select.ts
Normal file
13
packages/lib/src/discord/jsx/components/role-select.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
7
packages/lib/src/discord/jsx/components/section.ts
Normal file
7
packages/lib/src/discord/jsx/components/section.ts
Normal 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[]));
|
||||
}
|
||||
6
packages/lib/src/discord/jsx/components/separator.ts
Normal file
6
packages/lib/src/discord/jsx/components/separator.ts
Normal 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);
|
||||
}
|
||||
14
packages/lib/src/discord/jsx/components/string-select.ts
Normal file
14
packages/lib/src/discord/jsx/components/string-select.ts
Normal 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]),
|
||||
);
|
||||
}
|
||||
14
packages/lib/src/discord/jsx/components/text-input.ts
Normal file
14
packages/lib/src/discord/jsx/components/text-input.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
7
packages/lib/src/discord/jsx/components/text.ts
Normal file
7
packages/lib/src/discord/jsx/components/text.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { text } from '@/discord/components/builders';
|
||||
import type { TextElement } from './element.types';
|
||||
|
||||
export function Text(props: TextElement['props'] & { children: TextElement['children'] }) {
|
||||
const children = Array.isArray(props.children) ? props.children.join('') : props.children;
|
||||
return text(children);
|
||||
}
|
||||
6
packages/lib/src/discord/jsx/components/thumbnail.ts
Normal file
6
packages/lib/src/discord/jsx/components/thumbnail.ts
Normal 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);
|
||||
}
|
||||
13
packages/lib/src/discord/jsx/components/user-select.ts
Normal file
13
packages/lib/src/discord/jsx/components/user-select.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
3
packages/lib/src/discord/jsx/index.ts
Normal file
3
packages/lib/src/discord/jsx/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './components';
|
||||
export * from './jsx';
|
||||
export * from './runtime';
|
||||
2
packages/lib/src/discord/jsx/jsx-dev-runtime.ts
Normal file
2
packages/lib/src/discord/jsx/jsx-dev-runtime.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { jsxDEV, Fragment } from './runtime';
|
||||
export type { JSX } from './jsx';
|
||||
2
packages/lib/src/discord/jsx/jsx-runtime.ts
Normal file
2
packages/lib/src/discord/jsx/jsx-runtime.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { jsx, Fragment } from './runtime';
|
||||
export type { JSX } from './jsx';
|
||||
112
packages/lib/src/discord/jsx/jsx.ts
Normal file
112
packages/lib/src/discord/jsx/jsx.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
type ActionRow,
|
||||
type Button,
|
||||
type ChannelSelectMenu,
|
||||
type MentionableSelectMenu,
|
||||
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,
|
||||
type ModalSubmitInteractionData,
|
||||
} from '@projectdysnomia/dysnomia';
|
||||
import type {
|
||||
ButtonElement,
|
||||
ChannelSelectElement,
|
||||
ContainerElement,
|
||||
FileElement,
|
||||
GalleryElement,
|
||||
LabelElement,
|
||||
MediaElement,
|
||||
MentionableSelectElement,
|
||||
ModalElement,
|
||||
OptionElement,
|
||||
PremiumButtonElement,
|
||||
RoleSelectElement,
|
||||
SectionElement,
|
||||
SeparatorElement,
|
||||
StringSelectElement,
|
||||
TextElement,
|
||||
TextInputElement,
|
||||
ThumbnailElement,
|
||||
URLButtonElement,
|
||||
UserSelectElement,
|
||||
} from './components/element.types';
|
||||
|
||||
export type Component =
|
||||
| ActionRow
|
||||
| Button
|
||||
| StringSelectMenu
|
||||
| UserSelectMenu
|
||||
| RoleSelectMenu
|
||||
| MentionableSelectMenu
|
||||
| ChannelSelectMenu
|
||||
| TextInput
|
||||
| LabelComponent
|
||||
| ContainerComponent
|
||||
| TextDisplayComponent
|
||||
| SectionComponent
|
||||
| MediaGalleryComponent
|
||||
| SeparatorComponent
|
||||
| FileComponent
|
||||
| InteractionButton
|
||||
| URLButton
|
||||
| PremiumButton
|
||||
| ThumbnailComponent
|
||||
| ModalSubmitInteractionData;
|
||||
|
||||
export type StarKittenElement = Component | Promise<Component>;
|
||||
|
||||
export interface StarKittenElementClass {
|
||||
render: any;
|
||||
}
|
||||
|
||||
export interface StarKittenElementAttributesProperty {
|
||||
props: {};
|
||||
}
|
||||
|
||||
export interface StarKittenElementChildrenAttribute {
|
||||
children: {};
|
||||
}
|
||||
|
||||
export interface StarKittenIntrinsicElements {
|
||||
actionRow: { children: StarKittenElement | StarKittenElement[] };
|
||||
button: ButtonElement['props'];
|
||||
urlButton: URLButtonElement['props'];
|
||||
premiumButton: PremiumButtonElement['props'];
|
||||
modal: ModalElement['props'] & { children: StarKittenElement | StarKittenElement[] };
|
||||
label: LabelElement['props'] & { children: StarKittenElement | StarKittenElement[] };
|
||||
stringSelect: StringSelectElement['props'] & { children: StringSelectElement['children'] };
|
||||
option: OptionElement['props'];
|
||||
textInput: TextInputElement['props'];
|
||||
text: TextElement['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 {}
|
||||
}
|
||||
68
packages/lib/src/discord/jsx/runtime.ts
Normal file
68
packages/lib/src/discord/jsx/runtime.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as components from './components';
|
||||
|
||||
const intrinsicComponentMap: Record<string, (props: any) => any> = {
|
||||
actionRow: components.ActionRow,
|
||||
button: components.Button,
|
||||
container: components.Container,
|
||||
file: components.File,
|
||||
gallery: components.Gallery,
|
||||
label: components.Label,
|
||||
media: components.Media,
|
||||
mentionableSelect: components.MentionableSelect,
|
||||
modal: components.Modal,
|
||||
option: components.Option,
|
||||
premiumButton: components.PremiumButton,
|
||||
roleSelect: components.RoleSelect,
|
||||
section: components.Section,
|
||||
separator: components.Separator,
|
||||
stringSelect: components.StringSelect,
|
||||
text: components.Text,
|
||||
textInput: components.TextInput,
|
||||
thumbnail: components.Thumbnail,
|
||||
urlButton: components.URLButton,
|
||||
userSelect: components.UserSelect,
|
||||
};
|
||||
|
||||
export const Fragment = (props: { children: any }) => {
|
||||
return [...props.children];
|
||||
};
|
||||
|
||||
export function jsx(type: any, props: Record<string, any>) {
|
||||
if (typeof type === 'function') {
|
||||
return type(props);
|
||||
}
|
||||
|
||||
if (typeof type === 'string' && intrinsicComponentMap[type]) {
|
||||
return intrinsicComponentMap[type](props);
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
||||
export function jsxDEV(
|
||||
type: any,
|
||||
props: Record<string, any>,
|
||||
key: string | number | symbol,
|
||||
isStaticChildren: boolean,
|
||||
source: any,
|
||||
self: any,
|
||||
) {
|
||||
// console.log('JSX DEV', type, props);
|
||||
if (typeof type === 'function') {
|
||||
return type(props);
|
||||
}
|
||||
|
||||
if (typeof type === 'string' && intrinsicComponentMap[type]) {
|
||||
return intrinsicComponentMap[type](props);
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
props: { ...props, key },
|
||||
_source: source,
|
||||
_self: self,
|
||||
};
|
||||
}
|
||||
22
packages/lib/src/discord/jsx/types.d.ts
vendored
Normal file
22
packages/lib/src/discord/jsx/types.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
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: {};
|
||||
}
|
||||
|
||||
interface IntrinsicElements extends StarKittenIntrinsicElements {}
|
||||
}
|
||||
2
packages/lib/src/discord/pages/index.ts
Normal file
2
packages/lib/src/discord/pages/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './pages';
|
||||
export * from './subroutes';
|
||||
166
packages/lib/src/discord/pages/pages.ts
Normal file
166
packages/lib/src/discord/pages/pages.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { Constants, type InteractionContentEdit, type InteractionModalContent } from '@projectdysnomia/dysnomia';
|
||||
import type { CommandContext, ExecutableInteraction } from '../types';
|
||||
|
||||
export enum PageType {
|
||||
MODAL = 'modal',
|
||||
MESSAGE = 'message',
|
||||
FOLLOWUP = 'followup',
|
||||
}
|
||||
|
||||
export interface Page<T> {
|
||||
key: string;
|
||||
type?: PageType; // defaults to MESSAGE
|
||||
followUpFlags?: number;
|
||||
render: (
|
||||
ctx: PageContext<T>,
|
||||
) => (InteractionModalContent | InteractionContentEdit) | Promise<InteractionModalContent | InteractionContentEdit>;
|
||||
}
|
||||
|
||||
export interface PagesOptions<T> {
|
||||
pages: Record<string, Page<T>>;
|
||||
initialPage?: string;
|
||||
timeout?: number; // in seconds
|
||||
ephemeral?: boolean; // whether the initial message should be ephemeral
|
||||
useEmbeds?: boolean; // will not enable components v2
|
||||
initialStateData?: T; // initial state to merge with default state
|
||||
router?: (ctx: PageContext<T>) => string; // function to determine the next page key
|
||||
}
|
||||
|
||||
export interface PageState<T> {
|
||||
currentPage: string;
|
||||
timeoutAt: number; // timestamp in ms
|
||||
lastInteractionAt?: number; // timestamp in ms
|
||||
messageId?: string;
|
||||
channelId?: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface PageContext<T> {
|
||||
state: PageState<T>;
|
||||
custom_id: string; // current interaction custom_id
|
||||
interaction: ExecutableInteraction;
|
||||
goToPage: (pageKey: string) => Promise<InteractionContentEdit>;
|
||||
}
|
||||
|
||||
function createPageContext<T>(interaction: ExecutableInteraction, options: PagesOptions<T>, state: PageState<T>): PageContext<T> {
|
||||
return {
|
||||
state,
|
||||
interaction,
|
||||
custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : options.initialPage ?? 'root',
|
||||
goToPage: (pageKey: string) => {
|
||||
const page = options.pages[pageKey];
|
||||
state.currentPage = pageKey;
|
||||
if (!page) {
|
||||
throw new Error(`Page with key "${pageKey}" not found`);
|
||||
}
|
||||
return page.render(createPageContext(interaction, options, { ...state, currentPage: pageKey })) as Promise<InteractionContentEdit>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function defaultPageState<T>(options: PagesOptions<T>): PageState<T> {
|
||||
const timeoutAt = options.timeout ? Date.now() + options.timeout * 1000 : Infinity;
|
||||
return {
|
||||
currentPage: options.initialPage ?? options.pages[0].key,
|
||||
timeoutAt,
|
||||
lastInteractionAt: Date.now(),
|
||||
data: options.initialStateData ?? ({} as T),
|
||||
};
|
||||
}
|
||||
|
||||
function getPageState<T>(options: PagesOptions<T>, cmdCtx: CommandContext & { state: { __pageState?: PageState<T> } }) {
|
||||
const cmdState = cmdCtx.state;
|
||||
if ('__pageState' in cmdState && cmdState.__pageState) {
|
||||
return cmdState.__pageState as PageState<T>;
|
||||
}
|
||||
cmdState.__pageState = defaultPageState(options);
|
||||
return cmdState.__pageState as PageState<T>;
|
||||
}
|
||||
|
||||
function validateOptions<T>(options: PagesOptions<T>) {
|
||||
const keys = Object.keys(options.pages);
|
||||
const uniqueKeys = new Set(keys);
|
||||
if (uniqueKeys.size !== keys.length) {
|
||||
throw new Error('Duplicate page keys found');
|
||||
}
|
||||
}
|
||||
|
||||
function getFlags(options: PagesOptions<any>) {
|
||||
let flags = 0;
|
||||
if (options.ephemeral) {
|
||||
flags |= Constants.MessageFlags.EPHEMERAL;
|
||||
}
|
||||
if (!options.useEmbeds) {
|
||||
flags |= Constants.MessageFlags.IS_COMPONENTS_V2;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
export async function usePages<T>(options: PagesOptions<T>, interaction: ExecutableInteraction, cmdCtx: CommandContext) {
|
||||
if (interaction.isAutocomplete() || interaction.isPing()) {
|
||||
throw new Error('usePages cannot be used with autocomplete or ping interactions');
|
||||
}
|
||||
|
||||
const pagesInteraction = interaction;
|
||||
validateOptions(options);
|
||||
const pageState = getPageState(options, cmdCtx);
|
||||
const pageContext = createPageContext(pagesInteraction, options, pageState);
|
||||
const pageKey = options.router
|
||||
? options.router(pageContext)
|
||||
: pageContext.custom_id ?? options.initialPage ?? Object.keys(options.pages)[0];
|
||||
// if we have subroutes, we only want the main route from the page key
|
||||
const page = options.pages[pageKey.split(':')[0]] ?? options.pages[0];
|
||||
pageContext.state.currentPage = page.key;
|
||||
|
||||
if (page.type === PageType.MODAL && !pagesInteraction.isModalSubmit()) {
|
||||
// we don't defer modals and can't respond to a modal with a modal.
|
||||
const maybePromise = page.render(pageContext);
|
||||
const content = isPromise(maybePromise) ? await maybePromise : maybePromise;
|
||||
return await pagesInteraction.createModal(content as InteractionModalContent);
|
||||
}
|
||||
|
||||
if (page.type === PageType.FOLLOWUP) {
|
||||
if (!pageState.messageId) {
|
||||
throw new Error('Cannot send a followup message before an initial message has been sent');
|
||||
}
|
||||
const flags = page.type === PageType.FOLLOWUP ? page.followUpFlags ?? getFlags(options) : getFlags(options);
|
||||
await pagesInteraction.defer(flags);
|
||||
const maybePromise = page.render(pageContext);
|
||||
const content = isPromise(maybePromise) ? await maybePromise : maybePromise;
|
||||
return await pagesInteraction.createFollowup({
|
||||
flags,
|
||||
...wrapJSXContent(content),
|
||||
});
|
||||
}
|
||||
|
||||
if (pageState.messageId && (pagesInteraction.isMessageComponent() || pagesInteraction.isModalSubmit())) {
|
||||
await pagesInteraction.deferUpdate();
|
||||
const maybePromise = page.render(pageContext);
|
||||
const content = isPromise(maybePromise) ? await maybePromise : maybePromise;
|
||||
return await pagesInteraction.editMessage(pageState.messageId, wrapJSXContent(content));
|
||||
}
|
||||
|
||||
{
|
||||
await pagesInteraction.defer(getFlags(options));
|
||||
const maybePromise = page.render(pageContext);
|
||||
const content = isPromise(maybePromise) ? await maybePromise : maybePromise;
|
||||
const message = await pagesInteraction.createFollowup({
|
||||
flags: getFlags(options),
|
||||
...wrapJSXContent(content),
|
||||
});
|
||||
pageState.messageId = message.id;
|
||||
pageState.channelId = message.channel?.id;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
|
||||
return typeof (value as Promise<T>)?.then === 'function';
|
||||
}
|
||||
|
||||
function wrapJSXContent(content: any) {
|
||||
if ('type' in content) {
|
||||
return { components: [content] };
|
||||
}
|
||||
return content;
|
||||
}
|
||||
99
packages/lib/src/discord/pages/subroutes.ts
Normal file
99
packages/lib/src/discord/pages/subroutes.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { PartialEmoji } from '@projectdysnomia/dysnomia';
|
||||
import { actionRow, button, gallery, type ButtonOptions, type ContainerItems } from '@/discord/components';
|
||||
import type { PageContext } from './pages';
|
||||
|
||||
export function getSubrouteKey(prefix: string, subroutes: string[]) {
|
||||
return `${prefix}:${subroutes.join(':')}`;
|
||||
}
|
||||
|
||||
export function parseSubrouteKey(key: string, expectedPrefix: string, expectedLength: number, defaults: string[] = []) {
|
||||
const parts = key.split(':');
|
||||
if (parts[0] !== expectedPrefix) {
|
||||
throw new Error(`Unexpected prefix: ${parts[0]}`);
|
||||
}
|
||||
if (parts.length - 1 < expectedLength && defaults.length) {
|
||||
// fill in defaults
|
||||
parts.push(...defaults.slice(parts.length - 1));
|
||||
}
|
||||
if (parts.length !== expectedLength + 1) {
|
||||
throw new Error(`Expected ${expectedLength} subroutes, but got ${parts.length - 1}`);
|
||||
}
|
||||
return parts.slice(1);
|
||||
}
|
||||
|
||||
export function renderSubrouteButtons(
|
||||
currentSubroute: string,
|
||||
subRoutes: string[],
|
||||
subrouteIndex: number,
|
||||
prefix: string,
|
||||
subroutes: { label: string; value: string; emoji?: PartialEmoji }[],
|
||||
options?: Partial<ButtonOptions>,
|
||||
) {
|
||||
return subroutes
|
||||
.filter((sr) => sr !== undefined)
|
||||
.map(({ label, value, emoji }) => {
|
||||
const routes = [...subRoutes];
|
||||
routes[subrouteIndex] = currentSubroute == value ? '_' : value;
|
||||
return button(label, getSubrouteKey(prefix, routes), {
|
||||
...options,
|
||||
disabled: value === currentSubroute,
|
||||
emoji,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface SubrouteOptions {
|
||||
label: string;
|
||||
value: string;
|
||||
emoji?: PartialEmoji;
|
||||
}
|
||||
|
||||
export function renderSubroutes<T, CType = ContainerItems>(
|
||||
context: PageContext<T>,
|
||||
prefix: string,
|
||||
subroutes: (SubrouteOptions & {
|
||||
banner?: string;
|
||||
actionRowPosition?: 'top' | 'bottom';
|
||||
})[][],
|
||||
render: (currentSubroute: string, ctx: PageContext<T>) => CType,
|
||||
btnOptions?: Partial<ButtonOptions>,
|
||||
defaultSubroutes?: string[], // if not provided, will use the first option of each subroute
|
||||
): CType[] {
|
||||
const currentSubroutes = parseSubrouteKey(
|
||||
context.custom_id,
|
||||
prefix,
|
||||
subroutes.length,
|
||||
defaultSubroutes || subroutes.map((s) => s[0].value),
|
||||
);
|
||||
|
||||
const components = subroutes
|
||||
.filter((sr) => sr.length > 0)
|
||||
.map((srOpts, index) => {
|
||||
const opts = srOpts.filter((sr) => sr !== undefined);
|
||||
if (opts.length === 0) return undefined;
|
||||
// find the current subroute, or default to the first
|
||||
const sri = opts.findIndex((s) => s.value === currentSubroutes[index]);
|
||||
const current = opts[sri] || opts[0];
|
||||
const components = [];
|
||||
|
||||
const actionRow = actionRow(...renderSubrouteButtons(current.value, currentSubroutes, index, prefix, opts, btnOptions));
|
||||
|
||||
if (current.banner) {
|
||||
components.push(gallery({ url: current.banner }));
|
||||
}
|
||||
|
||||
if (!current.actionRowPosition || current.actionRowPosition === 'top') {
|
||||
components.push(actionRow);
|
||||
}
|
||||
|
||||
components.push(render(current.value, context));
|
||||
|
||||
if (current.actionRowPosition === 'bottom') {
|
||||
components.push(actionRow);
|
||||
}
|
||||
return components;
|
||||
})
|
||||
.flat()
|
||||
.filter((c) => c !== undefined);
|
||||
return components;
|
||||
}
|
||||
28
packages/lib/src/discord/types/command.type.ts
Normal file
28
packages/lib/src/discord/types/command.type.ts
Normal 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>;
|
||||
}
|
||||
2
packages/lib/src/discord/types/index.ts
Normal file
2
packages/lib/src/discord/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './command.type';
|
||||
export * from './interaction.type';
|
||||
25
packages/lib/src/discord/types/interaction.type.ts
Normal file
25
packages/lib/src/discord/types/interaction.type.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type Dysnomia from '@projectdysnomia/dysnomia';
|
||||
import type { StarKittenElement } from '../jsx';
|
||||
|
||||
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;
|
||||
createJSXMessage: (component: StarKittenElement) => Promise<Dysnomia.Message>;
|
||||
editJSXMessage: (messageID: string, component: StarKittenElement) => Promise<Dysnomia.Message>;
|
||||
createJSXFollowup: (component: StarKittenElement) => Promise<Dysnomia.Message>;
|
||||
createJSXModal: (component: StarKittenElement) => Promise<void>;
|
||||
}
|
||||
|
||||
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;
|
||||
14
packages/lib/src/eve/db/index.ts
Normal file
14
packages/lib/src/eve/db/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { join } from 'node:path';
|
||||
import { characters, resumeCommands, users, miningFleets, miningFleetParticipants } from './schema'; // Added mining tables
|
||||
|
||||
export const DB_PATH = process.env.AUTH_DB_PATH || join(process.cwd(), '../../db/kitten.db');
|
||||
console.log('Using DB_PATH:', DB_PATH);
|
||||
export * as schema from './schema';
|
||||
export * as models from './models';
|
||||
export * from './models';
|
||||
|
||||
// 'D:\\dev\\@star-kitten\\db\\kitten.db'
|
||||
const sqlite = new Database(DB_PATH);
|
||||
export const db = drizzle(sqlite, { schema: { users, characters, resumeCommands, miningFleets, miningFleetParticipants } });
|
||||
9
packages/lib/src/eve/db/migrate.ts
Normal file
9
packages/lib/src/eve/db/migrate.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { join } from 'node:path';
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { DB_PATH } from '.';
|
||||
|
||||
const sqlite = new Database(DB_PATH);
|
||||
const db = drizzle(sqlite);
|
||||
migrate(db, { migrationsFolder: join(process.cwd(), '/drizzle') });
|
||||
186
packages/lib/src/eve/db/models/character.model.ts
Normal file
186
packages/lib/src/eve/db/models/character.model.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { User } from './user.model';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { characters } from '../schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { db } from '..';
|
||||
import { ESI_SCOPE, refresh, verify, type EveTokens } from '@/eve/oauth';
|
||||
import { options } from '@/eve/esi';
|
||||
|
||||
export interface Character {
|
||||
id: number;
|
||||
eveID: number;
|
||||
userID: number;
|
||||
accessToken: string;
|
||||
expiresAt: Date;
|
||||
refreshToken: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export class CharacterHelper {
|
||||
public static hasValidToken(character: Character) {
|
||||
return new Date() < character.expiresAt;
|
||||
}
|
||||
|
||||
public static getScopes(character: Character) {
|
||||
const decoded = jwtDecode(character.accessToken) as {
|
||||
scp: string[] | string;
|
||||
};
|
||||
return typeof decoded.scp === 'string' ? [decoded.scp] : decoded.scp;
|
||||
}
|
||||
|
||||
public static hasOnlyPublicScope(character: Character) {
|
||||
return this.getScopes(character).length === 1 && this.hasScope(character, 'publicData');
|
||||
}
|
||||
|
||||
public static getTokens(character: Character) {
|
||||
return {
|
||||
access_token: character.accessToken,
|
||||
refresh_token: character.refreshToken,
|
||||
expires_in: (character.expiresAt.getTime() - Date.now()) / 1000,
|
||||
};
|
||||
}
|
||||
|
||||
public static hasScope(character: Character, scope: string) {
|
||||
return this.getScopes(character).includes(scope);
|
||||
}
|
||||
|
||||
public static hasAllScopes(character: Character, scopes: string[]) {
|
||||
const has = this.getScopes(character);
|
||||
return scopes.every((scope) => has.includes(scope));
|
||||
}
|
||||
|
||||
public static find(id: number) {
|
||||
const result = db.select().from(characters).where(eq(characters.id, id)).limit(1).get();
|
||||
const c = this.createCharacters(result);
|
||||
return c ? c[0] : undefined;
|
||||
}
|
||||
|
||||
public static findByUser(user: User) {
|
||||
const result = db.select().from(characters).where(eq(characters.userID, user.id)).all();
|
||||
return this.createCharacters(result);
|
||||
}
|
||||
|
||||
public static findByUserAndEveID(userID: number, eveID: number) {
|
||||
const result = db
|
||||
.select()
|
||||
.from(characters)
|
||||
.where(and(eq(characters.userID, userID), eq(characters.eveID, eveID)))
|
||||
.limit(1)
|
||||
.get();
|
||||
const c = this.createCharacters(result);
|
||||
return c ? c[0] : undefined;
|
||||
}
|
||||
|
||||
public static findByName(userID: number, name: string) {
|
||||
const result = db
|
||||
.select()
|
||||
.from(characters)
|
||||
.where(and(eq(characters.name, name), eq(characters.userID, userID)))
|
||||
.limit(1)
|
||||
.get();
|
||||
const c = this.createCharacters(result);
|
||||
return c ? c[0] : undefined;
|
||||
}
|
||||
|
||||
public static findAll() {
|
||||
const result = db.select().from(characters).all();
|
||||
return this.createCharacters(result);
|
||||
}
|
||||
|
||||
static create(eveID: number, name: string, user: User, tokens: EveTokens) {
|
||||
return this.save({
|
||||
eveID: eveID,
|
||||
userID: user.id,
|
||||
accessToken: tokens.access_token,
|
||||
expiresAt: new Date(tokens.expires_in * 1000),
|
||||
refreshToken: tokens.refresh_token,
|
||||
name: name,
|
||||
createdAt: new Date(),
|
||||
} as Character);
|
||||
}
|
||||
|
||||
static createCharacters(query: any): Character[] {
|
||||
if (!query) return [];
|
||||
if (Array.isArray(query)) {
|
||||
return query.map((character: any) => {
|
||||
return {
|
||||
id: character.id,
|
||||
eveID: character.eveID,
|
||||
userID: character.userID,
|
||||
accessToken: character.accessToken,
|
||||
expiresAt: new Date(character.expiresAt),
|
||||
refreshToken: character.refreshToken,
|
||||
name: character.name,
|
||||
createdAt: new Date(character.createdAt),
|
||||
updatedAt: new Date(character.updatedAt),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
id: query.id,
|
||||
eveID: query.eveID,
|
||||
userID: query.userID,
|
||||
accessToken: query.accessToken,
|
||||
expiresAt: new Date(query.expiresAt),
|
||||
refreshToken: query.refreshToken,
|
||||
name: query.name,
|
||||
createdAt: new Date(query.createdAt),
|
||||
updatedAt: new Date(query.updatedAt),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public static save(character: Character) {
|
||||
db.insert(characters)
|
||||
.values({
|
||||
id: character.id,
|
||||
eveID: character.eveID,
|
||||
userID: character.userID,
|
||||
name: character.name,
|
||||
accessToken: character.accessToken,
|
||||
expiresAt: character.expiresAt.getTime(),
|
||||
refreshToken: character.refreshToken,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: characters.id,
|
||||
set: {
|
||||
eveID: character.eveID,
|
||||
userID: character.userID,
|
||||
name: character.name,
|
||||
accessToken: character.accessToken,
|
||||
expiresAt: character.expiresAt.getTime(),
|
||||
refreshToken: character.refreshToken,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
})
|
||||
.run();
|
||||
return CharacterHelper.findByUserAndEveID(character.userID, character.eveID);
|
||||
}
|
||||
|
||||
public static delete(character: Character) {
|
||||
db.delete(characters).where(eq(characters.id, character.id)).run();
|
||||
}
|
||||
|
||||
public static async refreshTokens(character: Character, scopes?: ESI_SCOPE[] | ESI_SCOPE) {
|
||||
const tokens = await refresh(
|
||||
{ refresh_token: character.refreshToken },
|
||||
{ scopes, clientId: options.client_id, clientSecret: options.client_secret },
|
||||
);
|
||||
const decoded = await verify(tokens.access_token);
|
||||
if (!decoded) {
|
||||
console.error(`Failed to validate token for character ${character.eveID}`);
|
||||
return character;
|
||||
}
|
||||
character.accessToken = tokens.access_token;
|
||||
character.expiresAt = new Date(Date.now() + tokens.expires_in * 1000);
|
||||
character.refreshToken = tokens.refresh_token;
|
||||
this.save(character);
|
||||
return character;
|
||||
}
|
||||
}
|
||||
3
packages/lib/src/eve/db/models/index.ts
Normal file
3
packages/lib/src/eve/db/models/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './user.model';
|
||||
export * from './character.model';
|
||||
export * from './resume-command.model';
|
||||
75
packages/lib/src/eve/db/models/resume-command.model.ts
Normal file
75
packages/lib/src/eve/db/models/resume-command.model.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { db } from '..';
|
||||
import { resumeCommands } from '../schema';
|
||||
|
||||
export class ResumeCommand {
|
||||
id!: string;
|
||||
command!: string;
|
||||
params!: string;
|
||||
context!: string;
|
||||
created: Date = new Date();
|
||||
|
||||
private constructor() {
|
||||
this.created = new Date();
|
||||
}
|
||||
|
||||
public static find(messageId: string) {
|
||||
const result = db.select().from(resumeCommands)
|
||||
.where(eq(resumeCommands.id, messageId))
|
||||
.get();
|
||||
return this.createFromQuery(result);
|
||||
}
|
||||
|
||||
static create(messageId: string, command: string, params: any = {}, context: any = {}) {
|
||||
const resume = new ResumeCommand();
|
||||
resume.id = messageId;
|
||||
resume.command = command;
|
||||
resume.params = JSON.stringify(params);
|
||||
resume.context = JSON.stringify(context);
|
||||
return resume;
|
||||
}
|
||||
|
||||
static createFromQuery(query: any) {
|
||||
if (!query) return null;
|
||||
const resume = new ResumeCommand();
|
||||
resume.id = query.id;
|
||||
resume.command = query.command;
|
||||
resume.params = query.params;
|
||||
resume.context = query.context;
|
||||
resume.created = query.created;
|
||||
return resume;
|
||||
}
|
||||
|
||||
public save() {
|
||||
db.insert(resumeCommands)
|
||||
.values({
|
||||
id: this.id,
|
||||
command: this.command,
|
||||
params: this.params,
|
||||
context: this.context,
|
||||
createdAt: this.created.getTime(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: resumeCommands.id,
|
||||
set: {
|
||||
command: this.command,
|
||||
params: this.params,
|
||||
context: this.context,
|
||||
},
|
||||
}).run();
|
||||
return this;
|
||||
}
|
||||
|
||||
public delete() {
|
||||
db.delete(resumeCommands)
|
||||
.where(eq(resumeCommands.id, this.id))
|
||||
.run();
|
||||
}
|
||||
|
||||
static delete(messageId: string) {
|
||||
db.delete(resumeCommands)
|
||||
.where(eq(resumeCommands.id, messageId))
|
||||
.run();
|
||||
}
|
||||
|
||||
}
|
||||
155
packages/lib/src/eve/db/models/user.model.ts
Normal file
155
packages/lib/src/eve/db/models/user.model.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { CharacterHelper } from './character.model';
|
||||
import { db } from '..';
|
||||
import { characters, users } from '../schema';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
discordID: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
characterIDs: number[];
|
||||
mainCharacterID?: number;
|
||||
}
|
||||
|
||||
export class UserHelper {
|
||||
public static find(id: number) {
|
||||
const result = db.select({
|
||||
id: users.id,
|
||||
discordID: users.discordID,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
mainCharacterID: users.mainCharacter,
|
||||
characterIDsString: sql<string>`json_group_array(characters.id)`,
|
||||
}).from(users)
|
||||
.where(eq(users.id, id))
|
||||
.leftJoin(characters, eq(users.id, characters.userID))
|
||||
.get();
|
||||
return this.createFromQuery(result) as User;
|
||||
}
|
||||
|
||||
public static findByDiscordId(id: string) {
|
||||
const result = db.select({
|
||||
id: users.id,
|
||||
discordID: users.discordID,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
mainCharacterID: users.mainCharacter,
|
||||
characterIDsString: sql<string>`json_group_array(characters.id)`,
|
||||
}).from(users)
|
||||
.where(eq(users.discordID, id))
|
||||
.leftJoin(characters, eq(users.id, characters.userID))
|
||||
.get();
|
||||
return this.createFromQuery(result) as User;
|
||||
}
|
||||
|
||||
public static findAll() {
|
||||
const result = db.select({
|
||||
id: users.id,
|
||||
discordID: users.discordID,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
mainCharacterID: users.mainCharacter,
|
||||
characterIDsString: sql<string>`json_group_array(characters.id)`,
|
||||
}).from(users)
|
||||
.leftJoin(characters, eq(users.id, characters.userID))
|
||||
.all();
|
||||
return this.createFromQuery(result) as User[];
|
||||
}
|
||||
|
||||
public static findByCharacterId(id: number) {
|
||||
const result = db.select({
|
||||
id: users.id,
|
||||
discordID: users.discordID,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
mainCharacterID: users.mainCharacter,
|
||||
characterIDsString: sql<string>`json_group_array(characters.id)`,
|
||||
}).from(users)
|
||||
.leftJoin(characters, eq(users.id, characters.userID))
|
||||
.where(eq(characters.id, id))
|
||||
.all();
|
||||
return this.createFromQuery(result) as User;
|
||||
}
|
||||
|
||||
public static findByCharacterName(name: string) {
|
||||
const result = db.select({
|
||||
id: users.id,
|
||||
discordID: users.discordID,
|
||||
createdAt: users.createdAt,
|
||||
updatedAt: users.updatedAt,
|
||||
mainCharacterID: users.mainCharacter,
|
||||
characterIDsString: sql<string>`json_group_array(characters.id)`,
|
||||
}).from(users)
|
||||
.leftJoin(characters, eq(users.id, characters.userID))
|
||||
.where(eq(characters.name, name))
|
||||
.all();
|
||||
return this.createFromQuery(result) as User;
|
||||
}
|
||||
|
||||
public static createFromQuery(query: any): User | User[] {
|
||||
if (!query) return [];
|
||||
if (Array.isArray(query)) {
|
||||
return query.map((user: any) => {
|
||||
return {
|
||||
id: user.id,
|
||||
discordID: user.discordID,
|
||||
createdAt: new Date(user.createdAt),
|
||||
updatedAt: new Date(user.updatedAt),
|
||||
characterIDs: user.characterIDsString ? (JSON.parse(user.characterIDsString as any ?? '[]') as any[]).map(s => Number(s)).sort() : [],
|
||||
mainCharacterID: user.mainCharacterID,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
id: query.id,
|
||||
discordID: query.discordID,
|
||||
createdAt: new Date(query.createdAt),
|
||||
updatedAt: new Date(query.updatedAt),
|
||||
characterIDs: query.characterIDsString ? (JSON.parse(query.characterIDsString as any ?? '[]') as any[]).map(s => Number(s)).sort() : [],
|
||||
mainCharacterID: query.mainCharacterID,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static create(discordID: string): User {
|
||||
this.save({
|
||||
discordID: discordID,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as User);
|
||||
return this.findByDiscordId(discordID);
|
||||
}
|
||||
|
||||
public static save(user: User) {
|
||||
db.insert(users)
|
||||
.values({
|
||||
id: user.id,
|
||||
discordID: user.discordID,
|
||||
mainCharacter: user.mainCharacterID,
|
||||
createdAt: user.createdAt.getTime(),
|
||||
updatedAt: user.updatedAt.getTime(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: users.id,
|
||||
set: {
|
||||
discordID: user.discordID,
|
||||
mainCharacter: user.mainCharacterID,
|
||||
updatedAt: user.updatedAt.getTime(),
|
||||
},
|
||||
}).run();
|
||||
return user;
|
||||
}
|
||||
|
||||
public static delete(user: User) {
|
||||
db.delete(users)
|
||||
.where(eq(users.id, user.id))
|
||||
.run();
|
||||
}
|
||||
|
||||
public static getCharacter(user: User, index: number) {
|
||||
if (!user.characterIDs) return undefined;
|
||||
if (index >= user.characterIDs.length) return undefined;
|
||||
return CharacterHelper.find(user.characterIDs[index]);
|
||||
}
|
||||
}
|
||||
108
packages/lib/src/eve/db/schema.ts
Normal file
108
packages/lib/src/eve/db/schema.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { sqliteTable, text, integer, index, real } from 'drizzle-orm/sqlite-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
export const shared = {
|
||||
createdAt: integer('created_at').notNull(),
|
||||
updatedAt: integer('updated_at'),
|
||||
};
|
||||
|
||||
export const users = sqliteTable('users', {
|
||||
id: integer().primaryKey().unique().notNull(),
|
||||
discordID: text('discord_id').unique().notNull(),
|
||||
mainCharacter: integer('main_character'),
|
||||
...shared,
|
||||
}, (table) => [
|
||||
index('idx_discord_id').on(table.discordID),
|
||||
index('idx_main_character').on(table.mainCharacter),
|
||||
]);
|
||||
|
||||
export const usersRelations = relations(users, ({ one, many }) => ({
|
||||
characters: many(characters),
|
||||
main: one(characters, {
|
||||
fields: [users.mainCharacter],
|
||||
references: [characters.id]
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
export const characters = sqliteTable('characters', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
eveID: integer('eve_id').notNull(),
|
||||
userID: integer('user_id').notNull(),
|
||||
name: text().notNull(),
|
||||
accessToken: text('access_token').notNull(),
|
||||
expiresAt: integer('expires_at').notNull(),
|
||||
refreshToken: text('refresh_token').notNull(),
|
||||
...shared,
|
||||
}, (table) => [
|
||||
index('idx_user_id').on(table.userID),
|
||||
index('idx_eve_id').on(table.eveID),
|
||||
]);
|
||||
|
||||
export const charactersRelations = relations(characters, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [characters.userID],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
export const resumeCommands = sqliteTable('resumecommands', {
|
||||
id: text().primaryKey(),
|
||||
command: text().notNull(),
|
||||
params: text().notNull(),
|
||||
context: text().notNull(),
|
||||
...shared,
|
||||
});
|
||||
|
||||
|
||||
// --- Mining Fleet Module ---
|
||||
|
||||
export const miningFleets = sqliteTable('mining_fleets', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
creatorDiscordId: text('creator_discord_id').notNull(),
|
||||
startTime: integer('start_time').notNull(),
|
||||
endTime: integer('end_time'),
|
||||
status: text('status', { enum: ['configuring', 'active', 'ended', 'generating_report', 'completed', 'failed'] }).notNull().default('configuring'),
|
||||
taxRate: real('tax_rate').notNull().default(0),
|
||||
publicMessageId: text('public_message_id').unique(),
|
||||
publicChannelId: text('public_channel_id'),
|
||||
reportData: text('report_data'), // Store as JSON string
|
||||
creatorEphemeralMessageId: text('creator_ephemeral_message_id'),
|
||||
...shared,
|
||||
}, (table) => [
|
||||
index('idx_fleet_creator_discord_id').on(table.creatorDiscordId),
|
||||
index('idx_fleet_status').on(table.status),
|
||||
index('idx_fleet_public_message_id').on(table.publicMessageId),
|
||||
]);
|
||||
|
||||
export const miningFleetParticipants = sqliteTable('mining_fleet_participants', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
fleetId: integer('fleet_id').notNull().references(() => miningFleets.id, { onDelete: 'cascade' }), // Cascade delete participants if fleet is deleted
|
||||
characterId: integer('character_id').notNull().references(() => characters.id, { onDelete: 'cascade' }), // Reference characters table PK
|
||||
discordId: text('discord_id').notNull(), // Discord ID of the user who added the character
|
||||
role: text('role', { enum: ['miner', 'security', 'hauler'] }).notNull(),
|
||||
joinTime: integer('join_time').notNull(),
|
||||
...shared,
|
||||
}, (table) => [
|
||||
index('idx_participant_fleet_id').on(table.fleetId),
|
||||
index('idx_participant_character_id').on(table.characterId),
|
||||
index('idx_participant_discord_id').on(table.discordId),
|
||||
]);
|
||||
|
||||
export const miningFleetsRelations = relations(miningFleets, ({ many }) => ({
|
||||
participants: many(miningFleetParticipants),
|
||||
}));
|
||||
|
||||
export const miningFleetParticipantsRelations = relations(miningFleetParticipants, ({ one }) => ({
|
||||
fleet: one(miningFleets, {
|
||||
fields: [miningFleetParticipants.fleetId],
|
||||
references: [miningFleets.id],
|
||||
}),
|
||||
character: one(characters, {
|
||||
fields: [miningFleetParticipants.characterId],
|
||||
references: [characters.id],
|
||||
}),
|
||||
}));
|
||||
59
packages/lib/src/eve/esi/alliance.ts
Normal file
59
packages/lib/src/eve/esi/alliance.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* EVE Swagger Interface - Alliance Endpoints
|
||||
* https://developers.eveonline.com/api-explorer#/operations/GetAlliances
|
||||
*/
|
||||
import { esiFetch, type PublicEsiOptions } from './util/fetch';
|
||||
|
||||
/**
|
||||
* List all active player alliances
|
||||
* - This route is cached for an hour
|
||||
* @returns {number[]} - An array of all active player alliance ids
|
||||
*/
|
||||
export async function listAlliances(options?: PublicEsiOptions) {
|
||||
return await esiFetch<number[]>('/alliances/', options);
|
||||
}
|
||||
|
||||
interface AllianceInfo {
|
||||
creator_corporation_id: number;
|
||||
creator_id: number;
|
||||
date_founded: string;
|
||||
executor_corporation_id: number;
|
||||
faction_id: number;
|
||||
name: string;
|
||||
ticker: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about a specific alliance
|
||||
* - This route is cached for an hour
|
||||
* @param alliance_id Alliance id
|
||||
* @returns {AllianceInfo}
|
||||
*/
|
||||
export async function getAllianceInformation(alliance_id: number, options?: PublicEsiOptions) {
|
||||
return await esiFetch<Partial<AllianceInfo>>(`/alliances/${alliance_id}/`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all corporations in an alliance
|
||||
* - This route is cached for an hour
|
||||
* @param alliance_id Alliance id
|
||||
* @returns {number[]} - Array of corporation ids
|
||||
*/
|
||||
export async function listAllianceCorporations(alliance_id: number, options?: PublicEsiOptions) {
|
||||
return await esiFetch<number[]>(`/alliances/${alliance_id}/corporations/`, options);
|
||||
}
|
||||
|
||||
interface AllianceIcon {
|
||||
px128x128: string;
|
||||
px64x64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alliance icon
|
||||
* - This route is cached for an hour
|
||||
* @param alliance_id Alliance id
|
||||
* @returns {AllianceIcon}
|
||||
*/
|
||||
export async function getAllianceIcon(alliance_id: number, options?: PublicEsiOptions) {
|
||||
return await esiFetch<Partial<AllianceIcon>>(`/alliances/${alliance_id}/icons/`, options);
|
||||
}
|
||||
128
packages/lib/src/eve/esi/assets.ts
Normal file
128
packages/lib/src/eve/esi/assets.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* EVE ESI Assets API
|
||||
* https://developers.eveonline.com/api-explorer#/operations/GetCharactersCharacterIdAssets
|
||||
*/
|
||||
import { ESI_SCOPE } from '../oauth';
|
||||
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
|
||||
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
|
||||
import type { LocationFlag } from './types/location-flag';
|
||||
|
||||
export enum AssetLocationType {
|
||||
STATION = 'station',
|
||||
SOLAR_SYSTEM = 'solar_system',
|
||||
ITEM = 'item',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
export interface Asset {
|
||||
is_blueprint_copy: boolean;
|
||||
is_singleton: boolean;
|
||||
item_id: number;
|
||||
location_flag: LocationFlag;
|
||||
location_id: number;
|
||||
location_type: AssetLocationType;
|
||||
quantity: number;
|
||||
type_id: number;
|
||||
}
|
||||
|
||||
export function getCharacterAssets(options: EsiOptions, page: number = 1) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_assets.v1']);
|
||||
return esiFetch<Partial<Asset>[]>(`/characters/${character_id}/assets/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_ASSET,
|
||||
});
|
||||
}
|
||||
|
||||
export interface AssetLocation {
|
||||
item_id: number;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function getCharacterAssetLocations(options: EsiOptions, ids: number[]) {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length > 1000) throw 'Maximum of 1000 IDs can be requested at once';
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_assets.v1']);
|
||||
|
||||
return esiFetch<Partial<AssetLocation>[]>(`/characters/${character_id}/assets/locations/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_ASSET,
|
||||
});
|
||||
}
|
||||
|
||||
export interface AssetNames {
|
||||
item_id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function getCharacterAssetNames(options: EsiOptions, ids: number[]) {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length > 1000) throw 'Maximum of 1000 IDs can be requested at once';
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_assets.v1']);
|
||||
return esiFetch<Partial<AssetNames>[]>(`/characters/${character_id}/assets/names/`, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_ASSET,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorpAsset {
|
||||
is_blueprint_copy: boolean;
|
||||
is_singleton: boolean;
|
||||
item_id: number;
|
||||
location_flag: LocationFlag;
|
||||
location_id: number;
|
||||
location_type: 'station' | 'solar_system' | 'item' | 'other';
|
||||
quantity: number;
|
||||
type_id: number;
|
||||
}
|
||||
|
||||
export async function getCorporationAssets(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_corporation_assets.v1']);
|
||||
return await esiFetch<Partial<CorpAsset>[]>(`/corporations/${corporation_id}/assets/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_ASSET,
|
||||
});
|
||||
}
|
||||
|
||||
export interface AssetLocation {
|
||||
item_id: number;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCorporationAssetLocations(options: EsiOptions, corporation_id: number, ids: number[]) {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length > 1000) throw 'Maximum of 1000 IDs can be requested at once';
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_corporation_assets.v1']);
|
||||
return await esiFetch<Partial<AssetLocation>[]>(`/corporations/${corporation_id}/assets/locations/`, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_ASSET,
|
||||
});
|
||||
}
|
||||
|
||||
export interface AssetNames {
|
||||
item_id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function getCorporationAssetNames(options: EsiOptions, id: number, ids: number[]) {
|
||||
if (ids.length === 0) return [];
|
||||
if (ids.length > 1000) throw 'Maximum of 1000 IDs can be requested at once';
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-assets.read_corporation_assets.v1']);
|
||||
return await esiFetch<Partial<AssetNames>[]>(`/corporations/${id}/assets/names/`, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(ids),
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_ASSET,
|
||||
});
|
||||
}
|
||||
113
packages/lib/src/eve/esi/calendar.ts
Normal file
113
packages/lib/src/eve/esi/calendar.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* EVE ESI Caldendar Module
|
||||
*
|
||||
* This module provides functions to interact with EVE Online's ESI Calendar API,
|
||||
* allowing retrieval and management of calendar events for characters.
|
||||
*
|
||||
* ref: https://developers.eveonline.com/api-explorer#/operations/GetCharactersCharacterIdCalendar
|
||||
*/
|
||||
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
|
||||
import { tokenHasScopes } from '../oauth/eve-auth';
|
||||
import { ESI_SCOPE } from '../oauth';
|
||||
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
|
||||
|
||||
export interface CalendarEvent {
|
||||
event_date: string; // date-time string
|
||||
event_id: number;
|
||||
event_response: 'accepted' | 'declined' | 'tentative' | 'no_response';
|
||||
importance: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* List calendar event summaries for a character.
|
||||
*
|
||||
* Get 50 event summaries from the calendar. If no from_event ID is given, the resource will
|
||||
* return the next 50 chronological event summaries from now. If a from_event ID is
|
||||
* specified, it will return the next 50 chronological event summaries from after that event
|
||||
* - cached for 5 seconds
|
||||
*
|
||||
* @param options EsiOptions
|
||||
* @param from_event Event from which to get the next 50 chronological event summaries
|
||||
* @returns {Partial<CalendarEvent>[]}
|
||||
*/
|
||||
export async function listCalendarEventSummaries(options: EsiOptions, from_event?: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-calendar.read_calendar_events.v1']);
|
||||
return await esiFetch<Partial<CalendarEvent>[]>(`/characters/${character_id}/calendar/${from_event ?? '?from_event=' + from_event}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CalendarEventDetails {
|
||||
date: string; // date-time string
|
||||
duration: number; // in minutes
|
||||
event_id: number;
|
||||
importance: number;
|
||||
owner_id: number;
|
||||
owner_name: string;
|
||||
owner_type: 'eve_server' | 'corporation' | 'faction' | 'alliance' | 'character';
|
||||
response: 'accepted' | 'declined' | 'tentative' | 'no_response';
|
||||
text: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an event's details by its ID.
|
||||
*
|
||||
* Get all the information for a specific event.
|
||||
* - cached for 5 seconds
|
||||
*
|
||||
* @param options EsiOptions
|
||||
* @param event_id Event Id
|
||||
* @returns {Partial<CalendarEventDetails>}
|
||||
*/
|
||||
export async function getEventDetails(options: EsiOptions, event_id: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-calendar.read_calendar_events.v1']);
|
||||
return await esiFetch<Partial<CalendarEventDetails>>(`/characters/${character_id}/calendar/${event_id}/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to a calendar event.
|
||||
*
|
||||
* Accept, decline, or tentatively accept an event invitation.
|
||||
*
|
||||
* @param options EsiOptions
|
||||
* @param event_id Event Id
|
||||
* @param response Response: 'accepted' | 'declined' | 'tentative'
|
||||
*/
|
||||
export async function respondToEvent(options: EsiOptions, event_id: number, response: 'accepted' | 'declined' | 'tentative') {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-calendar.respond_calendar_events.v1']);
|
||||
return await esiFetch<void>(`/characters/${character_id}/calendar/${event_id}/`, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ response }),
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CalendarEventAttendee {
|
||||
character_id: number;
|
||||
event_response: 'accepted' | 'declined' | 'tentative' | 'no_response';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attendees of a calendar event.
|
||||
*
|
||||
* Get the list of attendees for a specific event.
|
||||
* - cached for 5 seconds
|
||||
*
|
||||
* @param options EsiOptions
|
||||
* @param event_id Event Id
|
||||
* @returns {Partial<CalendarEventAttendee>[]}
|
||||
*/
|
||||
export async function getEventAttendees(options: EsiOptions, event_id: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-calendar.read_calendar_events.v1']);
|
||||
return await esiFetch<Partial<CalendarEventAttendee>[]>(`/characters/${character_id}/calendar/${event_id}/attendees/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
220
packages/lib/src/eve/esi/character.ts
Normal file
220
packages/lib/src/eve/esi/character.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { ESI_SCOPE } from '../oauth';
|
||||
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
|
||||
import type { NotificationType } from './types/notification-type';
|
||||
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions, type PublicEsiOptions } from './util/fetch';
|
||||
import type { Blueprint } from './types/shared';
|
||||
export interface CharacterAffiliations {
|
||||
character_id: number;
|
||||
corporation_id: number;
|
||||
alliance_id?: number;
|
||||
faction_id?: number;
|
||||
}
|
||||
export function getCharacterAffiliations(character_ids: number[], options?: PublicEsiOptions) {
|
||||
if (character_ids.length === 0) return [];
|
||||
if (character_ids.length > 1000) throw 'Maximum of 1000 character IDs can be requested at once';
|
||||
return esiFetch<Partial<CharacterAffiliations>[]>(`/characters/affiliation/`, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(character_ids),
|
||||
});
|
||||
}
|
||||
|
||||
export interface CharacterData {
|
||||
alliance_id: number;
|
||||
birthday: string;
|
||||
bloodline_id: number;
|
||||
corporation_id: number;
|
||||
description: string;
|
||||
faction_id: number;
|
||||
gender: 'male' | 'female';
|
||||
name: string;
|
||||
race_id: number;
|
||||
security_status: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function getCharacterPublicData(id: number, options?: PublicEsiOptions) {
|
||||
return esiFetch<Partial<CharacterData>>(`/characters/${id}/`, options);
|
||||
}
|
||||
|
||||
export interface AgentResearch {
|
||||
agent_id: number;
|
||||
points_per_day: number;
|
||||
remainder_points: number;
|
||||
skill_type_id: number;
|
||||
started_at: string;
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_agents_research.v1
|
||||
export function getCharacterAgentResearch(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_agents_research.v1']);
|
||||
return esiFetch<Partial<AgentResearch>[]>(`/characters/${character_id}/agents_research/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_INDUSTRY,
|
||||
});
|
||||
}
|
||||
|
||||
// required scope: esi-characters.read_blueprints.v1
|
||||
export function getCharacterBlueprints(options: EsiOptions, page: number = 1) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_blueprints.v1']);
|
||||
return esiFetch<Partial<Blueprint>[]>(`/characters/${character_id}/blueprints/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_INDUSTRY,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CharacterCorporationHistory {
|
||||
corporation_id: number;
|
||||
is_deleted: boolean;
|
||||
record_id: number; // An incrementing ID that can be used to order records where start_date is ambiguous
|
||||
start_date: string;
|
||||
}
|
||||
|
||||
export function getCharacterCorporationHistory(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_corporation_roles.v1']);
|
||||
return esiFetch<Partial<CharacterCorporationHistory>[]>(`/characters/${character_id}/corporationhistory/`, options);
|
||||
}
|
||||
|
||||
export function calculateCSPAChargeCost(options: EsiOptions, target_character_ids: number[]) {
|
||||
if (target_character_ids.length === 0) return null;
|
||||
if (target_character_ids.length > 100) throw 'Maximum of 100 target character IDs can be requested at once';
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_cspa.v1']);
|
||||
return esiFetch<number[]>(`/characters/${character_id}/cspa/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(target_character_ids),
|
||||
});
|
||||
}
|
||||
|
||||
export interface JumpFatigue {
|
||||
jump_fatigue_expire_date: string;
|
||||
last_jump_date: string;
|
||||
last_update_date: string;
|
||||
}
|
||||
|
||||
export function getCharacterJumpFatigue(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_fatigue.v1']);
|
||||
return esiFetch<Partial<JumpFatigue>>(`/characters/${character_id}/fatigue/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface Medals {
|
||||
corporation_id: number;
|
||||
date: string;
|
||||
description: string;
|
||||
graphics: {
|
||||
color: number;
|
||||
graphic: number;
|
||||
layer: number;
|
||||
part: number;
|
||||
}[];
|
||||
issuer_id: number;
|
||||
medal_id: number;
|
||||
reason: string;
|
||||
status: 'private' | 'public';
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function getCharacterMedals(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_medals.v1']);
|
||||
return esiFetch<Partial<Medals>[]>(`/characters/${character_id}/medals/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
is_read: boolean;
|
||||
notification_id: number;
|
||||
sender_id: number;
|
||||
sender_type: 'character' | 'corporation' | 'alliance' | 'faction' | 'other';
|
||||
text: string;
|
||||
timestamp: string;
|
||||
type: NotificationType;
|
||||
}
|
||||
|
||||
export function getCharacterNotifications(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_notifications.v1']);
|
||||
return esiFetch<Partial<Notification>[]>(`/characters/${character_id}/notifications/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_NOTIFICATION,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ContactNotification {
|
||||
message: string;
|
||||
notification_id: number;
|
||||
send_date: string;
|
||||
sender_character_id: number;
|
||||
standing_level: -10 | -5 | 0 | 5 | 10;
|
||||
}
|
||||
|
||||
export function getCharacterContactNotifications(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_notifications.v1']);
|
||||
return esiFetch<Partial<ContactNotification>[]>(`/characters/${character_id}/notifications/contacts`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CharacterPortraits {
|
||||
px128x128: string;
|
||||
px256x256: string;
|
||||
px512x512: string;
|
||||
px64x64: string;
|
||||
}
|
||||
|
||||
export function getCharacterPortraits(character_id: number, options?: PublicEsiOptions) {
|
||||
return esiFetch<Partial<CharacterPortraits>>(`/characters/${character_id}/portrait/`, options);
|
||||
}
|
||||
|
||||
export function getPortraitURL(character_id: number) {
|
||||
return `https://images.evetech.net/characters/${character_id}/portrait`;
|
||||
}
|
||||
|
||||
export interface CharacterCorporationRoles {
|
||||
roles: string[];
|
||||
roles_at_base: string[];
|
||||
roles_at_hq: string[];
|
||||
roles_at_other: string[];
|
||||
}
|
||||
|
||||
export function getCharacterCorporationRoles(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_corporation_roles.v1']);
|
||||
return esiFetch<Partial<CharacterCorporationRoles>>(`/characters/${character_id}/roles`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CharacterStandings {
|
||||
from_id: number;
|
||||
from_type: 'agent' | 'npc_corp' | 'faction';
|
||||
standing: number;
|
||||
}
|
||||
|
||||
export function getCharacterStandings(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_standings.v1']);
|
||||
return esiFetch<Partial<CharacterStandings>[]>(`/characters/${character_id}/standings`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CharacterTitles {
|
||||
titles: {
|
||||
name: string;
|
||||
title_id: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function getCharacterTitles(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_titles.v1']);
|
||||
return esiFetch<Partial<CharacterTitles>>(`/characters/${character_id}/titles`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
|
||||
});
|
||||
}
|
||||
35
packages/lib/src/eve/esi/clones.ts
Normal file
35
packages/lib/src/eve/esi/clones.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ESI_SCOPE } from '../oauth/auth.types';
|
||||
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
|
||||
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
|
||||
|
||||
export interface CharacterClones {
|
||||
home_location: {
|
||||
location_id: number;
|
||||
location_type: 'station' | 'structure';
|
||||
};
|
||||
jump_clones: {
|
||||
implants: number[];
|
||||
jump_clone_id: number;
|
||||
location_id: number;
|
||||
location_type: 'station' | 'structure';
|
||||
name: string;
|
||||
}[];
|
||||
last_clone_jump_date: string;
|
||||
last_station_change_date: string;
|
||||
}
|
||||
|
||||
export function getCharacterClones(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-clones.read_clones.v1']);
|
||||
return esiFetch<Partial<CharacterClones>>(`/characters/${character_id}/clones`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_LOCATION,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCharacterActiveImplants(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-clones.read_implants.v1']);
|
||||
return esiFetch<number[]>(`/characters/${character_id}/implants`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_DETAIL,
|
||||
});
|
||||
}
|
||||
133
packages/lib/src/eve/esi/contacts.ts
Normal file
133
packages/lib/src/eve/esi/contacts.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { ESI_SCOPE } from '../oauth/auth.types';
|
||||
import type { STANDING } from './types/shared';
|
||||
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions } from './util/fetch';
|
||||
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
|
||||
|
||||
export interface Contact {
|
||||
contact_id: number;
|
||||
contact_type: 'character' | 'corporation' | 'alliance' | 'faction';
|
||||
label_ids?: number[];
|
||||
standing: STANDING;
|
||||
}
|
||||
|
||||
export function getAllianceContacts(options: EsiOptions, alliance_id: number, page: number = 1) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-alliances.read_contacts.v1']);
|
||||
return esiFetch<Contact[]>(`/alliances/${alliance_id}/contacts/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.ALLIANCE_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ContactLabel {
|
||||
label_id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function getAllianceContactLabels(options: EsiOptions, alliance_id: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-alliances.read_contacts.v1']);
|
||||
return esiFetch<ContactLabel[]>(`/alliances/${alliance_id}/contacts/labels/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.ALLIANCE_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteCharacterContacts(options: EsiOptions, character_ids: number[]) {
|
||||
if (character_ids.length === 0) return;
|
||||
if (character_ids.length > 20) throw 'Maximum of 20 IDs can be deleted at once';
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.write_contacts.v1']);
|
||||
return esiFetch<void>(`/characters/${character_id}/contacts/`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ contact_ids: character_ids }),
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CharacterContact extends Contact {
|
||||
is_blocked?: boolean;
|
||||
is_watched?: boolean;
|
||||
}
|
||||
|
||||
export function getCharacterContacts(options: EsiOptions, page: number = 1) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_contacts.v1']);
|
||||
return esiFetch<CharacterContact[]>(`/characters/${character_id}/contacts/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export function addCharacterContacts(
|
||||
options: EsiOptions,
|
||||
character_ids: number[],
|
||||
standing: STANDING,
|
||||
label_ids?: number[],
|
||||
watched?: boolean,
|
||||
) {
|
||||
if (character_ids.length === 0) return;
|
||||
if (character_ids.length > 100) throw 'Maximum of 100 IDs can be added at once';
|
||||
if (label_ids && label_ids.length > 63) throw 'Maximum of 63 label IDs can be assigned at once';
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.write_contacts.v1']);
|
||||
return esiFetch<void>(
|
||||
`/characters/${character_id}/contacts?standing=${standing}${watched ? '&watched=true' : ''}${
|
||||
label_ids ? `&label_ids=${label_ids.join(',')}` : ''
|
||||
}`,
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(character_ids),
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function editCharacterContacts(
|
||||
options: EsiOptions,
|
||||
character_ids: number[],
|
||||
standing: STANDING,
|
||||
label_ids?: number[],
|
||||
watched?: boolean,
|
||||
) {
|
||||
if (character_ids.length === 0) return;
|
||||
if (character_ids.length > 100) throw 'Maximum of 100 IDs can be edited at once';
|
||||
if (label_ids && label_ids.length > 63) throw 'Maximum of 63 label IDs can be assigned at once';
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.write_contacts.v1']);
|
||||
return esiFetch<void>(
|
||||
`/characters/${character_id}/contacts?standing=${standing}${watched ? '&watched=true' : ''}${
|
||||
label_ids ? `&label_ids=${label_ids.join(',')}` : ''
|
||||
}`,
|
||||
{
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(character_ids),
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getCharacterContactLabels(options: EsiOptions) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-characters.read_contacts.v1']);
|
||||
return esiFetch<Partial<ContactLabel>[]>(`/characters/${character_id}/contacts/labels/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationContact extends Contact {
|
||||
is_watched?: boolean;
|
||||
}
|
||||
|
||||
export function getCorporationContacts(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_contacts.v1']);
|
||||
return esiFetch<CorporationContact[]>(`/corporations/${corporation_id}/contacts/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_SOCIAL,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCorporationContactLabels(options: EsiOptions, corporation_id: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_contacts.v1']);
|
||||
return esiFetch<ContactLabel[]>(`/corporations/${corporation_id}/contacts/labels/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_SOCIAL,
|
||||
});
|
||||
}
|
||||
135
packages/lib/src/eve/esi/contracts.ts
Normal file
135
packages/lib/src/eve/esi/contracts.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { ESI_SCOPE } from '../oauth/auth.types';
|
||||
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
|
||||
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions, type PublicEsiOptions } from './util/fetch';
|
||||
|
||||
export interface PublicContract {
|
||||
buyout?: number;
|
||||
collateral?: number;
|
||||
contract_id: number;
|
||||
date_expired: string;
|
||||
date_issued: string;
|
||||
days_to_complete?: number;
|
||||
end_location_id?: number;
|
||||
for_corporation: boolean;
|
||||
issuer_corporation_id: number;
|
||||
issuer_id: number;
|
||||
price?: number;
|
||||
reward?: number;
|
||||
start_location_id?: number;
|
||||
title?: string;
|
||||
type: 'item_exchange' | 'auction' | 'courier' | 'loan';
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
export interface Contract extends PublicContract {
|
||||
acceptor_id: number;
|
||||
assignee_id: number;
|
||||
availability: 'public' | 'personal' | 'corporation' | 'alliance';
|
||||
date_accepted?: string;
|
||||
date_completed?: string;
|
||||
status:
|
||||
| 'outstanding'
|
||||
| 'in_progress'
|
||||
| 'finished_issuer'
|
||||
| 'finished_contractor'
|
||||
| 'cancelled'
|
||||
| 'rejected'
|
||||
| 'failed'
|
||||
| 'deleted'
|
||||
| 'reversed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns contracts available to a character, only if the character is issuer, acceptor or assignee.
|
||||
* Only returns contracts no older than 30 days, or if the status is "in_progress".
|
||||
*/
|
||||
export function getCharacterContracts(options: EsiOptions, page: number = 1) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_character_contracts.v1']);
|
||||
return esiFetch<Contract[]>(`/characters/${character_id}/contracts/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_CONTRACT,
|
||||
});
|
||||
}
|
||||
|
||||
export interface PublicContractBid {
|
||||
amount: number;
|
||||
bid_id: number;
|
||||
date_bid: string;
|
||||
}
|
||||
|
||||
export interface ContractBid extends PublicContractBid {
|
||||
bidder_id: number;
|
||||
}
|
||||
|
||||
export function getContractBids(options: EsiOptions, contract_id: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_character_contracts.v1']);
|
||||
return esiFetch<ContractBid[]>(`/characters/${character_id}/contracts/${contract_id}/bids/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_CONTRACT,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ContractItem {
|
||||
is_included: boolean; // true if the item is included in the contract, false if it is being requested
|
||||
is_singleton: boolean;
|
||||
quantity: number; // number of items (for stackable items)
|
||||
raw_quantity?: number; // -1 indicates that the item is a singleton (non-stackable). If the item happens to be a Blueprint, -1 is an Original and -2 is a Blueprint Copy
|
||||
record_id: number; // unique ID for this item in the contract
|
||||
type_id: number; // type ID of the item
|
||||
}
|
||||
|
||||
export function getContractItems(options: EsiOptions, contract_id: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_character_contracts.v1']);
|
||||
return esiFetch<ContractItem[]>(`/characters/${character_id}/contracts/${contract_id}/items/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CHAR_CONTRACT,
|
||||
});
|
||||
}
|
||||
|
||||
export function getPublicContractBids(contract_id: number, page: number = 1, options?: PublicEsiOptions) {
|
||||
return esiFetch<PublicContractBid[]>(`/contracts/public/bids/${contract_id}?page=${page}`, options);
|
||||
}
|
||||
|
||||
export interface PublicContractItem {
|
||||
is_blueprint_copy?: boolean;
|
||||
is_included: boolean; // true if the item is included in the contract, false if it is being requested
|
||||
item_id: number;
|
||||
material_efficiency?: number; // Material efficiency level of the blueprint
|
||||
quantity: number;
|
||||
record_id: number; // unique ID for this item in the contract
|
||||
runs?: number; // Number of runs for the blueprint
|
||||
time_efficiency?: number; // Time efficiency level of the blueprint
|
||||
type_id: number; // type ID of the item
|
||||
}
|
||||
|
||||
export function getPublicContractItems(contract_id: number, page: number = 1, options?: PublicEsiOptions) {
|
||||
return esiFetch<PublicContractItem[]>(`/contracts/public/items/${contract_id}?page=${page}`, options);
|
||||
}
|
||||
|
||||
export function getPublicContracts(region_id: number, page: number = 1, options?: PublicEsiOptions) {
|
||||
return esiFetch<PublicContract[]>(`/contracts/public/${region_id}?page=${page}`, options);
|
||||
}
|
||||
|
||||
export function getCorporationContracts(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_corporation_contracts.v1']);
|
||||
return esiFetch<Contract[]>(`/corporations/${corporation_id}/contracts/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_CONTRACT,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCorporationContractBids(options: EsiOptions, corporation_id: number, contract_id: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_corporation_contracts.v1']);
|
||||
return esiFetch<ContractBid[]>(`/corporations/${corporation_id}/contracts/${contract_id}/bids/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_CONTRACT,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCorporationContractItems(options: EsiOptions, corporation_id: number, contract_id: number) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-contracts.read_corporation_contracts.v1']);
|
||||
return esiFetch<ContractItem[]>(`/corporations/${corporation_id}/contracts/${contract_id}/items/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_CONTRACT,
|
||||
});
|
||||
}
|
||||
67
packages/lib/src/eve/esi/corporation-projects.ts
Normal file
67
packages/lib/src/eve/esi/corporation-projects.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Eve Corporation Projects ESI endpoints
|
||||
* ref: https://developers.eveonline.com/api-explorer
|
||||
*/
|
||||
import { ESI_SCOPE } from '../oauth/auth.types';
|
||||
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
|
||||
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions, type PublicEsiOptions } from './util/fetch';
|
||||
import type { Blueprint, Icons, STANDING } from './types/shared';
|
||||
import type { CorporationRoles } from './types/corporation';
|
||||
|
||||
export interface CorporationProject {
|
||||
id: string; // uuid
|
||||
last_modified: string;
|
||||
name: string;
|
||||
progress: {
|
||||
current: number;
|
||||
desired: number;
|
||||
};
|
||||
reward: {
|
||||
initial: number; // original ISK amount reserved for the project
|
||||
remaining: number; // ISK amount still remaining for the project
|
||||
};
|
||||
state: 'Unspecified' | 'Active' | 'Closed' | 'Completed' | 'Expired' | 'Deleted';
|
||||
/**
|
||||
* State:
|
||||
* Unspecified - An unspecified state
|
||||
* Active - Active and accepting contributions
|
||||
* Closed - Closed by the corporation
|
||||
* Completed - Completed
|
||||
* Expired - Expired
|
||||
* Deleted - Deleted and the details are no longer available
|
||||
*/
|
||||
}
|
||||
|
||||
export interface CorporationProjectResponse {
|
||||
cursor?: {
|
||||
after?: string; // cursor to use as 'after' in your next request, to continue walking fowards in time
|
||||
before?: string; // cursor to use as 'before' in your next request, to continue walking backwards in time
|
||||
};
|
||||
|
||||
projects: CorporationProject[];
|
||||
}
|
||||
|
||||
export function listCorporationProjects(
|
||||
options: EsiOptions,
|
||||
corporation_id: number,
|
||||
filters: {
|
||||
after?: string;
|
||||
before?: string;
|
||||
limit?: number;
|
||||
state?: 'Unspecified' | 'Active' | 'Closed' | 'Completed' | 'Expired' | 'Deleted';
|
||||
} = {},
|
||||
) {
|
||||
const character_id = checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_projects.v1']);
|
||||
const queryParams = new URLSearchParams();
|
||||
if (filters.after) queryParams.append('after', filters.after);
|
||||
if (filters.before) queryParams.append('before', filters.before);
|
||||
if (filters.limit) queryParams.append('limit', filters.limit.toString());
|
||||
if (filters.state) queryParams.append('state', filters.state);
|
||||
|
||||
return esiFetch<CorporationProjectResponse>(`/corporations/${corporation_id}/projects/?${queryParams.toString()}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_PROJECT,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: finish the remaining endpoints later
|
||||
360
packages/lib/src/eve/esi/corporation.ts
Normal file
360
packages/lib/src/eve/esi/corporation.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Eve Corporation ESI endpoints
|
||||
* ref: https://developers.eveonline.com/api-explorer#/operations/GetCorporationsNpccorps
|
||||
*/
|
||||
import { ESI_SCOPE } from '../oauth/auth.types';
|
||||
import { ESI_RATE_LIMIT_GROUP } from './util/rate-limits';
|
||||
import { checkScopesAndGetCharacterId, esiFetch, type EsiOptions, type PublicEsiOptions } from './util/fetch';
|
||||
import type { Blueprint, Icons, STANDING } from './types/shared';
|
||||
import type { CorporationRoles } from './types/corporation';
|
||||
|
||||
export async function getNpcCorporations(options?: PublicEsiOptions) {
|
||||
return await esiFetch<number[]>('/corporations/npccorps', options);
|
||||
}
|
||||
|
||||
interface CorporationInfo {
|
||||
alliance_id?: number;
|
||||
ceo_id: number;
|
||||
creator_id: number;
|
||||
date_founded?: string;
|
||||
description?: string;
|
||||
faction_id?: number;
|
||||
home_station_id?: number;
|
||||
member_count: number;
|
||||
name: string;
|
||||
shares?: number;
|
||||
tax_rate: number;
|
||||
ticker: string;
|
||||
url?: string;
|
||||
war_eligible?: boolean;
|
||||
}
|
||||
|
||||
export async function getCorporationData(corporation_id: number, options?: PublicEsiOptions) {
|
||||
return await esiFetch<CorporationInfo>(`/corporations/${corporation_id}/`, options);
|
||||
}
|
||||
|
||||
interface AllianceHistory {
|
||||
alliance_id?: number;
|
||||
is_deleted?: boolean;
|
||||
record_id: number;
|
||||
start_date: string;
|
||||
}
|
||||
|
||||
export async function getCorporationAllianceHistory(corporation_id: number, options?: PublicEsiOptions) {
|
||||
return await esiFetch<AllianceHistory[]>(`/corporations/${corporation_id}/alliancehistory/`, options);
|
||||
}
|
||||
|
||||
export async function getCorporationBlueprints(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_blueprints.v1']);
|
||||
return await esiFetch<Partial<Blueprint>[]>(`/corporations/${corporation_id}/blueprints/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_INDUSTRY,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllCorporationALSCLogs(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_container_logs.v1']);
|
||||
return await esiFetch<any[]>(`/corporations/${corporation_id}/containers/logs/?page=${page}`, {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationDivisions {
|
||||
hangar: {
|
||||
division: number;
|
||||
name: string;
|
||||
}[];
|
||||
wallet: {
|
||||
division: number;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export async function getCorporationDivisions(options: EsiOptions, corporation_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_divisions.v1']);
|
||||
return await esiFetch<CorporationDivisions>(`/corporations/${corporation_id}/divisions/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_WALLET,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationFacility {
|
||||
facility_id: number;
|
||||
system_id: number;
|
||||
type_id: number;
|
||||
}
|
||||
|
||||
export async function getCorporationFacilities(options: EsiOptions, corporation_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_facilities.v1']);
|
||||
return await esiFetch<CorporationFacility[]>(`/corporations/${corporation_id}/facilities/`, {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCorporationIcons(corporation_id: number, options?: PublicEsiOptions) {
|
||||
return await esiFetch<Icons>(`/corporations/${corporation_id}/icons/`, options);
|
||||
}
|
||||
|
||||
export interface CorporationMedal {
|
||||
created_at: string;
|
||||
createor_id: number;
|
||||
description: string;
|
||||
medal_id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function getCorporationMedals(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_medals.v1']);
|
||||
return await esiFetch<CorporationMedal[]>(`/corporations/${corporation_id}/medals/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationIssuedMedal {
|
||||
character_id: number;
|
||||
issued_at: string;
|
||||
issuer_id: number;
|
||||
medal_id: number;
|
||||
reason: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export async function getCorporationIssuedMedals(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_medals.v1']);
|
||||
return await esiFetch<CorporationIssuedMedal[]>(`/corporations/${corporation_id}/medals/issued/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCorporationMembers(options: EsiOptions, corporation_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_corporation_membership.v1']);
|
||||
return await esiFetch<number[]>(`/corporations/${corporation_id}/members/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCorporationMemberLimit(options: EsiOptions, corporation_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.track_members.v1']);
|
||||
return await esiFetch<number>(`/corporations/${corporation_id}/members/limit/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationMemberTitles {
|
||||
character_id: number;
|
||||
titles: number[];
|
||||
}
|
||||
|
||||
export async function getCorporationMemberTitles(options: EsiOptions, corporation_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_titles.v1']);
|
||||
return await esiFetch<CorporationMemberTitles[]>(`/corporations/${corporation_id}/members/titles/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationMemberTracking {
|
||||
base_id?: number;
|
||||
character_id: number;
|
||||
location_id?: number;
|
||||
logoff_date?: string;
|
||||
logon_date?: string;
|
||||
ship_type_id?: number;
|
||||
start_date?: string;
|
||||
}
|
||||
|
||||
export async function getCorporationMemberTracking(options: EsiOptions, corporation_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.track_members.v1']);
|
||||
return await esiFetch<CorporationMemberTracking[]>(`/corporations/${corporation_id}/membertracking/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationMemberRole {
|
||||
character_id: number;
|
||||
grantable_roles: CorporationRoles[];
|
||||
grantable_roles_at_base: CorporationRoles[];
|
||||
grantable_roles_at_hq: CorporationRoles[];
|
||||
grantable_roles_at_other: CorporationRoles[];
|
||||
roles: CorporationRoles[];
|
||||
roles_at_base: CorporationRoles[];
|
||||
roles_at_hq: CorporationRoles[];
|
||||
roles_at_other: CorporationRoles[];
|
||||
}
|
||||
|
||||
export async function getCorporationMemberRoles(options: EsiOptions, corporation_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_corporation_membership.v1']);
|
||||
return await esiFetch<CorporationMemberRole[]>(`/corporations/${corporation_id}/members/roles/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationMemberRoleHistory {
|
||||
changed_at: string;
|
||||
character_id: number;
|
||||
issuer_id: number;
|
||||
new_roles: CorporationRoles[];
|
||||
old_roles: CorporationRoles[];
|
||||
role_type:
|
||||
| 'grantable_roles'
|
||||
| 'grantable_roles_at_base'
|
||||
| 'grantable_roles_at_hq'
|
||||
| 'grantable_roles_at_other'
|
||||
| 'roles'
|
||||
| 'roles_at_base'
|
||||
| 'roles_at_hq'
|
||||
| 'roles_at_other';
|
||||
}
|
||||
|
||||
export async function getCorporationMemberRoleHistory(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_corporation_membership.v1']);
|
||||
return await esiFetch<CorporationMemberRoleHistory[]>(`/corporations/${corporation_id}/roles/history/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationShareholder {
|
||||
share_count: number;
|
||||
shareholder_id: number;
|
||||
shareholder_type: 'character' | 'corporation';
|
||||
}
|
||||
|
||||
export async function getCorporationShareholders(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-wallet.read_corporation_wallets.v1']);
|
||||
return await esiFetch<CorporationShareholder[]>(`/corporations/${corporation_id}/shareholders/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationStanding {
|
||||
from_id: number;
|
||||
from_type: 'agent' | 'npc_corp' | 'faction';
|
||||
standing: STANDING;
|
||||
}
|
||||
|
||||
export async function getCorporationStandings(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_standings.v1']);
|
||||
return await esiFetch<CorporationStanding[]>(`/corporations/${corporation_id}/standings/?page=${page}`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_MEMBER,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationStarbase {
|
||||
moon_id?: number;
|
||||
onlined_since?: string;
|
||||
reinforced_until?: string;
|
||||
starbase_id: number;
|
||||
state?: 'offline' | 'online' | 'onlining' | 'reinforced' | 'unanchoring';
|
||||
system_id: number;
|
||||
type_id: number;
|
||||
unanchor_at?: string;
|
||||
}
|
||||
|
||||
export async function getCorporationStarbases(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_starbases.v1']);
|
||||
return await esiFetch<CorporationStarbase[]>(`/corporations/${corporation_id}/starbases/?page=${page}`, {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export type StarbaseRole = 'alliance_member' | 'config_starbase_equipment_role' | 'corporation_member' | 'starbase_fuel_technician_role';
|
||||
|
||||
export interface StarbaseDetail {
|
||||
allow_alliance_members: boolean;
|
||||
allow_corporation_members: boolean;
|
||||
anchor: StarbaseRole;
|
||||
attack_if_at_war: boolean;
|
||||
attach_if_other_security_status_dropping: boolean;
|
||||
attack_security_status_threshold?: number;
|
||||
attack_standing_threshold?: STANDING;
|
||||
fuel_bay_take: StarbaseRole;
|
||||
fuel_bay_view: StarbaseRole;
|
||||
fuels?: {
|
||||
type_id: number;
|
||||
quantity: number;
|
||||
}[];
|
||||
offline: StarbaseRole;
|
||||
online: StarbaseRole;
|
||||
unanchor: StarbaseRole;
|
||||
use_alliance_standings: boolean;
|
||||
}
|
||||
|
||||
export async function getCorporationStarbaseDetail(options: EsiOptions, corporation_id: number, starbase_id: number, system_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_starbases.v1']);
|
||||
return await esiFetch<StarbaseDetail>(`/corporations/${corporation_id}/starbases/${starbase_id}/?system_id=${system_id}`, {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export interface StationService {
|
||||
name: string;
|
||||
state: 'online' | 'offline' | 'cleanup';
|
||||
}
|
||||
|
||||
export interface CorporationStructure {
|
||||
corporation_id: number;
|
||||
fuel_expires: string;
|
||||
name: string;
|
||||
next_reinforce_apply: string;
|
||||
next_reinforce_hour: number;
|
||||
profile_id: number;
|
||||
reinforce_hour: number;
|
||||
services: StationService[];
|
||||
state:
|
||||
| 'anchor_vulnerable'
|
||||
| 'anchoring'
|
||||
| 'armor_reinforce'
|
||||
| 'armor_vulnerable'
|
||||
| 'deploy_vulnerable'
|
||||
| 'fitting_invulnerable'
|
||||
| 'hull_reinforce'
|
||||
| 'hull_vulnerable'
|
||||
| 'online_depreceated'
|
||||
| 'onlining_vulnerable'
|
||||
| 'shield_vulnerable'
|
||||
| 'unanchored'
|
||||
| 'unknown';
|
||||
state_timer_end: string; // date at which the structure will move to it's next state
|
||||
state_timer_start: string; // date at which the structue entered it's current state
|
||||
structure_id: number;
|
||||
system_id: number;
|
||||
type_id: number;
|
||||
unanchors_at: string;
|
||||
}
|
||||
|
||||
export async function getCorporationStructures(options: EsiOptions, corporation_id: number, page: number = 1) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_structures.v1']);
|
||||
return await esiFetch<CorporationStructure[]>(`/corporations/${corporation_id}/structures/?page=${page}`, {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CorporationTitle {
|
||||
grantable_roles: CorporationRoles[];
|
||||
grantable_roles_at_base: CorporationRoles[];
|
||||
grantable_roles_at_hq: CorporationRoles[];
|
||||
grantable_roles_at_other: CorporationRoles[];
|
||||
name: string;
|
||||
roles: CorporationRoles[];
|
||||
roles_at_base: CorporationRoles[];
|
||||
roles_at_hq: CorporationRoles[];
|
||||
roles_at_other: CorporationRoles[];
|
||||
title_id: number;
|
||||
}
|
||||
|
||||
export async function getCorporationTitles(options: EsiOptions, corporation_id: number) {
|
||||
checkScopesAndGetCharacterId(options, ESI_SCOPE['esi-corporations.read_titles.v1']);
|
||||
return await esiFetch<CorporationTitle[]>(`/corporations/${corporation_id}/titles/`, {
|
||||
...options,
|
||||
rateLimitGroup: ESI_RATE_LIMIT_GROUP.CORP_DETAIL,
|
||||
});
|
||||
}
|
||||
1
packages/lib/src/eve/esi/dogma.ts
Normal file
1
packages/lib/src/eve/esi/dogma.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/lib/src/eve/esi/faction-warfare.ts
Normal file
1
packages/lib/src/eve/esi/faction-warfare.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/lib/src/eve/esi/fittings.ts
Normal file
1
packages/lib/src/eve/esi/fittings.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/lib/src/eve/esi/fleets.ts
Normal file
1
packages/lib/src/eve/esi/fleets.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/lib/src/eve/esi/freelance-jobs.ts
Normal file
1
packages/lib/src/eve/esi/freelance-jobs.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/lib/src/eve/esi/incursions.ts
Normal file
1
packages/lib/src/eve/esi/incursions.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user