Initial commit

This commit is contained in:
JB
2026-01-14 20:21:44 -05:00
commit e9865d3ee8
237 changed files with 15121 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY_DEVELOPMENT="02572da3d4f3a844588a944214c0e142a5a01deaa6551456af146d34b574024416"
# .env.development
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY="02292a330aa041b5f7efc51504e0c208accba67a6877a217ab43cbb59c3c0c3e66"
# .env
DEBUG="encrypted:BMnswwe5JnAjCOEIHrmuuuOmcSKep3PBL9i1EPP+6/luAaY5Ad696SA6G1xvXV1xSf7CaIemi9y06hBVUD2mL0F1ZWYXB6LAtjQeGzG6/HnWdatUszmLg6eEkXQa5fHC+MkgnII="
PORT="encrypted:BJxm0HAN4oxgITjzqsung1vfXRfUaHOUac1NPGeQFoKpSrGPmbvfD4fkIREyO+qsD68f+Swp0yCYiB6Z7crRepAxn9qQLK0UGAuI+JHMhRP0PuK/a3FyCMe+9L+XIaMs82SnrFc="
NODE_ENV="encrypted:BKJvsZtYfKMgKzFSA6vBXj2insCZcUZrFUIYdcfd4ze5nG4qpgiOKpiBQQ6EAsrda03/wqV8xfOGfHExVx68+35+tjazC+0Qr/+YBebBvn9MGruLDYB4BeOA8H8Cl1EwW7MCULLoMn1aaS8d"
LOG_LEVEL="encrypted:BD5xrc2gWbqC7kLwh9Nu9AeJh0+23WMARX878CEpASauF4o4udsLS4qYT1y9ayVHPvWD5Nj+yWhXhDO7/LU+MmtFH9lNS9lnJrZJcMIkTTGh8l854PdXC91RXFHx9xRvVORRcLYt"
BASE_URL="encrypted:BMHCtunmFrbJgkqEHsYvbulEUkBFfB7HU1eL1Aldm4qKB7ao445FZ4v/tfwjh2P1lG+BLnJkYM3syubLmVwdtJO9JvGlYdQC+TFCtFYiQQAMjK+2pyQljt/NvhlVioHfe/k7jjSNR3IhWEeRmzyPrCHTcUo77tuLkHvGQXFbSQ=="
EVE_CLIENT_ID="encrypted:BPZqIsv1pNA1W88QK+D4Tn2jJy6Ek/5YZT377LrG8R+zFUk/MIFQ9byCgDMIzsfjAyLwaHpiDggVKLYL7+REgsJ0+Qp8u9jWjOCFtP/Z+HqOnJi5H6ZtWCoWKVOHbMsYyjUD1u6w09hqkWDUSS859UWrno8SnA4O1gNThpawuiID"
EVE_CLIENT_SECRET="encrypted:BCPsdV2aOikGdRsIZrEm+wlYo5c5/XlpCNJYWXDy+I8jYgtHsgCFeRSe7cyjOnMMxs/Wnu54xcBIzu+eOekSe6vzFBTN4DmJKc4TAROmuppUxpyAlhi+ZH6QJXNCix4kHftXVhgolaf8EfpeQYfVSjJAmJVWCF0483KoDdtX0IU1zOkq7as1EzHA6QWP"
EVE_CALLBACK_URL="encrypted:BLWHVLxXGWYPmX/FAN0GBRq7mne/fjJRql8r0EZuHPrcNzOe9CxnplYlJrnUac6MpicWp81EDc5SKIfkKlDfMOAxQvHlAWQukEN7T4XMOr2LROdtoJPV/mg634foH670ZnIp0SoMrS2jziYCYIP15GjLlBc3uMD1G2lT/L+oYaI9chz4Gi7eJ8Uu04KMK5zTPw=="
ESI_USER_AGENT="encrypted:BDSR40896Vx8+5v1VLL+Y1g7AofntT3Hky13zAM/8bO5OizB17WuJS6AGK18FiTKs76ZT7FJd8IOf++OvcPuLLlHAcq4r8W69v1fgK74Y0cpMw3eGd9+iapHGZKQJnTfKWhW33HSl+J1GqNRXCgGfzGN2BdCjDn4t4l/l5C/oSJega3f0zwt5xcLU/cH4meXpHx6CJF4FP0r3QYYY5eWvIPe+kETA4PlSmKcXivnWQA3qLk4P51/q+w="
DISCORD_APP_ID="encrypted:BO6M18yRDBLEnb9sVWZM4KJyI6HXPEt6h+g3eJhkAPAiiiZqtftXdJ5kRWH4tbZSnyREiTyKwCZmu/79UEuEKrNM/t0IvVGcYRevkmIkQbcv7pmNdFGULWIuDiXwp7DI65o1Y7Lc+SyTZtF0w6xPn6k+fg=="
DISCORD_APP_SECRET="encrypted:BCECawQcSwdBhWZD7u7fTir3vnISlzboluoP7HM4vIcor8HLq5FniVSrrahEi6FQ9/D7GnE1accQFHw5v05DMYGDC6Pk8MteVvNDBsb26D+9ysW+lPMS475mRapiAX5uxsyful61ndlexcbqffYdCbQz3sCCZ0V9T1qL6OmfUDcZ"
DISCORD_PUBLIC_KEY="encrypted:BHiB0kNDdo7jkzGpLNLG7cZhM/x6kHR2xoo9Qq6/UptSCRdTbRIIreUh91nNzs34HKJHuIShS47F1gUF/4AgFawfvosNFWGLysQRw8/BO1jPQ7xkXkOPFvFRjtn8ofAKkLequwcq/tLTJdUnXk32pghLCnLFf+JLYkwZdIiqWNlPEm+DTa2d8GJe9j+ix6fOlI9FWsRo+cY0UPpDKXZ7KgU="
DISCORD_BOT_TOKEN="encrypted:BI+BUi9br2kQ3dkmpLA+ZJG6m0ggwWJuBfsSEFOcaXctqOV9h3obbjbFf37UrKMXk4DdGobFCHWFc2gwygKwIyYV9mt6TSTNT3R3RbxV0wVwJHn66Ln7+Of9RaXQSr3rmLwMrKm23RiBuILTeqNaYdgrj2BJfNvCH4ld73jjglbfM0nrnuNZV3bi3IOiEfDizsLTy9nnVGHyd5hjDoE+LBbN6HH7QxI="
DISCORD_TEST_GUILD_ID="encrypted:BOtjgCwLkmVwj1T5nqFpiIuf5p4HdRxk0En8PyVPjPR4ICeUCD44brUtcOZveysfadU+UVH0TfWUNmdozmEl1XDs045jaJa4tcQodrEqvUUyW0P0ZvKoRNQsVV/ZnBYcxtSLqPB2OBGKZlQuDy2M50gSPw=="
JANICE_KEY="encrypted:BLHC4JqLJ3gjIXK7voNFQV89houZhR/d+3WT6hZZWaADlrTFTcmNl6bB0xd/yCjTUQ4w1SKl5IXptlidICaJpEHqc0vSTVZ7WHzI3QpZvTeUAYPQ6OF1yYKKv5ddwFDDDDTkQkAC+Kv3WMlEaHA05CVMybceQeqgCVal9/+xTTSG"
PERPLEXITY_API_KEY="encrypted:BHagAU/l8HMkXMvgQOaz76bIv+kjPBpseTbJf9mRBbJWfGmq5zvI0kQL5OciiCSOIC9E+INPpWC5TrtACTkthrBxwQtq27bEw3QumnvLNn2S9jK/Y50u3iy9j7YL3LcsDd2SmnWSklmADPZP55TbdGOxRJO8EsI4WxYoppL/XLtT6Lwp7FjHpYX8ge7z7ROGhGANB+zf"
STAR_KITTEN_KV_DB_PATH="encrypted:BBCp6CWMlFZoHM6aWTTJxV8RFpOoSYSwzH7VkHllQgkarnr+pitwAshI4K8JvYGfGLZOmiZshMRrQ1QsK5AbN4tij+rTnRO3yuq1kX5Nbdu1/77FeBZpaA7TXVG1Bj2QovWzdgongEUlew4lJGbv"
SCOPES=encrypted:BDDahsMJtV00qzcrwCfdt4eaLikrMUQXzBU+n82EKWZDzvxPY7G6tifGQC9M3GEIpDCZQSBc5gkjiTspWg/taGRA4CxEAvmYVmiskGZ8t1ZE0RjQ24ulPzfg/R0aFfXZIsB4HTgk85PinRzT2RuxSwfzmz4bwQ1yGGVQRs46Oy5m5hS7LvnBV7jPuWIqixSkawL5n+5hrREFN3Lfpw==
CONCIERGE_DB_PATH="encrypted:BNRvsmopAUFW1qFGJr1UuV0A7+/tZeVD6jhxt2JzZsfkiB8YKFoEOJuJU6k+3NYzIzDMCKdwvTa611u4dEVlWbQQ8yyHTra1YtU5JgZYd+SsPFlA02APnPgy8NN8oMhiCFkdYb1JSq8jDjkLTYbvwwenwJP99Q=="

View File

@@ -0,0 +1,23 @@
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY_PRODUCTION="02f0469506f6722d8fcc179c199ff159ca32f082000c8e7a1465891adb50a4c031"
# .env.production
DEBUG="encrypted:BJ05zNQ0KPsEKFER9ZlvQxEFJgc5rbw0k4j65AIPgyy/kZkV7prGEyaCtXZLmCUsqtM0NyMMEl7bbLcLeBmr2AbaR0+U7yBIqQnUi46uIV27vZO9fdfCC6Z4SiFaHUXfgl6s1S/Z"
PORT="encrypted:BLPq+hz1+WPsCKxKEKFsK+EBvlf4iBwxfsjPN+FFSV078Bk+p8ihS2i5GrXd5JTIW9NJBeT4ZhnL5GXbKY5/iwoj7MmQe9WIYJ+SKBeZXXUyxGGfsE+Hd9u5XWRdbfYJQ8QCnI4="
NODE_ENV="encrypted:BE+VACL1v6wLsm3Se5A+xajeH7a6LJ9gj1cdm5R+y6IlG92/p20XU9IBdWqJ00FnMB0cPZfPx2odnSq+TzcqOSYMCWFcgD4nC7suefU08EKa8A2YGxjO2WxkQo/EC7dQO+eIGlWzdimDS98="
LOG_LEVEL="encrypted:BNXP0WNyTPrhszuDNaRsE9jFc51VtuUqfCs/EnCaP9iRvXmbekmsQzfbrNPSmHw9HBHWjPbzJUgFfebXgR2qd65L173luLUPOHu0wF5T0zaBc6zt9VdkQbJhdaDWN3FnHkWc4oM="
BASE_URL="encrypted:BGY3awxPETBuzUz1uP+jJq4u9J9xumGjRGLtccdza6lDPCl7DNf3Tv/GjbfJPH1aXkqvOfGSs6Mj6X2Sv5cNfwJYpP67HoQuNn94g93JjkGesRpkgvLv8pOAB1RaS6F3SkGHosdGOex+wyYeVf5rNhY1vZA7B3SX"
DISCORD_APP_ID="encrypted:BBV+j87Z6p/suAQ4HRdC4VOGyGNUhltobM3yaRLOC7SF0/XBw4bMx/OOpXE5qqfQdeyAhTpkEEzwH1BzaIiAZFJouMSyKYN/Gw1rbkB9LEzN9Yz6SuCZaub4ftwzZW5pJ8adzSjfm9JLFegmMO8MzJkoZU4="
DISCORD_APP_SECRET="encrypted:BLC+chabTKr/7iLjutqOlA8r9vAn3DkGN14fUxgjRGs7udyvXUKXDYsVd8SQPC5/iraYL23+Qw8PBTnTh2P8SRq4tQq/jO0H8KHrJY+DfgjJqt2f3KK19rFvDvvEBWtvFIXwF1PrWcCrIF0T3IYn1Nq9/KRED6pbjK/HERF+q4RC"
DISCORD_PUBLIC_KEY="encrypted:BAv92AM4VtQdyAqtLiCeFRuaMZ+6cazOMCNr6454p3PLIZDiMAbYEPzzIqyYZFWzMUJDTXx6c91I7dX33UZKG5vIB/tGiUrvHIRgnXXsbz4ZwXXYRZ0gDKzAFFyDaNk95RO8gZdvRdAAVUlTO/t2wlwEmGVj61jULNIdtuQBMa8tdl9XkhSAoq8O3oiHeKCPCCKU9QuiUJReU22KwZ0rPIk="
DISCORD_BOT_TOKEN="encrypted:BOafG2kHKOp209oFlXUURj3yt7JLIOJbD9dChXFY1GKJMjV63LFBhyjZkpZsrXjcrc3r/FR026UlTiO1SIDxBLlHlla2yN3ip+jLl2wAD8jJWsch+HrQPH34G9C4Peo56wIptiUIr6ug9PqHqjAE5WClEJTFwSPPHgx1btHpoyxgWnqe7h0tbc2talmfOS/8xvJ+9TKRbL165MX/+HjPJI9+7kMqrHGNJA=="
EVE_CLIENT_ID="encrypted:BPSEia/l0Aea+iX3Q5citAbtxkTBIFlBRwNhxoV4BVT4rbmGr0heJLb9ymS3srfHhlh6kra2oZzpH15UVyISL6lTxSi72CsvgsvX9cO8GdCUywSC94XBDLV5XjGMQ4vuL3ce3KDaWW+di28+7sxttAmU/b97F+547wpFkFnjb0ir"
EVE_CLIENT_SECRET="encrypted:BGru3muBXbNenhZ+e4259tQOqELbhogNa5x0Yxxw2gbj7uiMb/KzzeaJ8WTBNFrlhS4NLKRE+EwAZp1LRKZVXMVAgCoF465c5zDZrHKaBi5SEjDnffKZOLzcpOeZgTbPMfFexGZqWuFgTtP2Bn2bu0p1MxXX5uEj++ZK2IfEONbHXzUnWRQYUts="
EVE_CALLBACK_URL="encrypted:BCHObZ6cAfm/V79x2fhqnAAzinxyFpiRiQY/wojKQ5QJS8KEJ1FCc89Ee5tVBXDDxFRPaBwkj/g4blKpKWAWLl7s3LCZJCYpPoPIkrDOOMop1JFAMnVGrLbK6Ird2agVc+SkMbjUurffQ7pLrdhNQKK0y7vW5K6DzsoMu7klq7QiUFBbcfKMW5E5"
ESI_USER_AGENT="encrypted:BC6tfyTm69Id4WT/csv81UxUUtmpZKoTwcV1HoZ1xSrx8+tJMIxnFVL4SxMcEa1pQXgpKgDDpSvoAaFZwndRHxSTZvfFvpoIZlUaljzzAdevUGh3+OhCFGvx9y8YwrlT4LB/L412sZD6NhgCX6rksU1iYREexj+7Kc54/sHsD7zYAIJ9y+PU+gvRDejx32oEhBEOdksnKplaTq2ApF3NCEBWt9eiB2rho6eltro="
AUTH_DB_PATH="encrypted:BO7oCtnSoclMPTL8j/8HqQe+6p5+u4xWtEuScqPV+u1Wr/bPls9rd8DSohIwe0Y00nnhi/oSqnExCB0ip3rmQ1YA62ZfFxvmOmHwgk1MNHfCG6bE3NurHR2NE4BdDCvh+yOQ8LcN04V+Ef+tkyjKBD241H3MYmdw"
JANICE_KEY="encrypted:BO4yawcMyniT7HH8apRtRv8uwNBFuQPRz3o8FPu7viTO+uGNMVPmoRKwI+mDjFc9JHRiIsnyOvjiOzDuojdWvoyKuilNKwpyuzkTCqjd2G7YaaYnurOkLZSllb2US/BvhN4Put04aqyGwpXyq2Ns34z080TjE0Q3oIJwgI6fSfR6"
PERPLEXITY_API_KEY="encrypted:BCY8v0hPEk0n6VhTJuJZeff9XOZrwqqK77UqgF5PLTfwfiKUHLRUbj3e84FydRFGTRvRx3a0QqP2PAf0JgqCt5B9Xdop2curOTptmc2Z9wDoshMFl0X15xXQzFz4kMmeb3P1uJtB9RVsU1BMGMtX76wNcW9+vsNzu37PW+s0OrIJlXw3QicPYybzenQT6KUl9IMTkP+X"

View File

@@ -0,0 +1,182 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
.env.keys
.flaskenv*
!.env.project
!.env.vault
data/
db/
litefs/
brainstorming/
# Sentry Config File
.sentryclirc
cloudflared.exe

View File

@@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120
}

View File

@@ -0,0 +1,8 @@
trailingComma: all
tabWidth: 2
useTabs: false
semi: true
singleQuote: true
printWidth: 140
experimentalTernaries: true
quoteProps: consistent

View File

@@ -0,0 +1,15 @@
# concierge-bot
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@@ -0,0 +1,29 @@
{
"name": "concierge-bot",
"type": "module",
"module": "src/main.ts",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"@dotenvx/dotenvx": "^1.49.0",
"@types/bun": "^1.2.21",
"prettier": "^3.6.2",
"typescript": "^5"
},
"dependencies": {
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
"@star-kitten/lib": "workspace:^0.0.0"
},
"scripts": {
"dev": "bunx dotenvx run -f .env.development -- bun run --watch src/main.ts",
"start": "bunx @dotenvx/dotenvx run -f .env.production -- bun src/main.ts",
"build": "mkdirp ./db && bun build --minify --sourcemap src/main.ts --target bun --outdir ./dist",
"lint": "prettier --check .",
"format": "prettier --write .",
"test": "bun test",
"encrypt": "bunx dotenvx encrypt -f .env.development && bunx dotenvx encrypt -f .env.production",
"decrypt": "bunx dotenvx decrypt -f .env.development && bunx dotenvx decrypt -f .env.production"
}
}

View File

@@ -0,0 +1,220 @@
import { componentHasIdPrefix, isModalLabel, text } from '@star-kitten/lib/discord/components';
import { createChatCommand, type CommandContext, type ExecutableInteraction } from '@star-kitten/lib/discord';
import { PageType, usePages } from '@star-kitten/lib/discord/pages';
import { StructureType, type Location } from '@/lib/db/location';
import { getDB } from '@/lib/db';
import { Constants, type ComponentInteractionSelectMenuData } from '@projectdysnomia/dysnomia';
interface LocationsState {
selected?: Location;
}
export default createChatCommand(
{
name: 'locations',
description: 'location management',
},
async (interaction: ExecutableInteraction, commandCtx: CommandContext) => {
await usePages<LocationsState>(
{
pages: {
main: {
key: 'main',
type: PageType.MESSAGE,
render: async (pageCtx) => {
const locations = getDB().getAllLocations();
console.log('Rendering locations page with locations:', locations);
const renderLocations = () => {
if (locations.length === 0) {
return 'No locations added yet.';
}
return locations
.map(
(loc) =>
`${loc.location_id}\t${loc.short_name}\t${loc.can_jf ? 'JF' : ''}${loc.can_dst ? ' DST' : ''}${loc.can_br ? ' BR' : ''}${loc.can_smb ? ' SMB' : ''}${loc.can_bridge ? ' BRIDGE' : ''}`,
)
.join('\n');
};
return (
<container accent={0x11cc33}>
<text>{`# Locations\n${renderLocations()}`}</text>
<actionRow>
<stringSelect
customId="edit-services"
placeholder="Select Location to Edit Services"
minValues={1}
maxValues={1}
>
{locations.map((loc) => (
<option label={loc.short_name} value={loc.location_id.toString()} />
))}
</stringSelect>
</actionRow>
<actionRow>
<button customId="add-location" label="Add Location" style={Constants.ButtonStyles.PRIMARY} />
</actionRow>
</container>
);
},
},
'add-location': {
key: 'add-location',
type: PageType.MODAL,
render: async (ctx) => {
return (
<modal title="Add Location" customId="add-location-modal">
<label label="Location Name">
<textInput customId="location-name" placeholder="Enter the location name" />
</label>
<label label="Location Short Name">
<textInput customId="location-short-name" placeholder="Enter the location short name" />
</label>
<label label="Structure Type">
<stringSelect
customId="structure-type"
placeholder="Select Structure Type"
minValues={1}
maxValues={1}
>
{Object.values(StructureType).map((type) => (
<option label={type} value={type} />
))}
</stringSelect>
</label>
</modal>
);
},
},
'edit-services': {
key: 'edit-services',
type: PageType.MODAL,
render: async (ctx) => {
return (
<modal
title={`Edit Location Services at ${ctx.state.data.selected?.short_name || ''}:`}
customId="edit-services-modal"
>
<label label="Can JF">
<stringSelect customId="can-jf" placeholder="Can JF" minValues={1} maxValues={1}>
<option label="Yes" value="yes" />
<option label="No" value="no" />
</stringSelect>
</label>
<label label="Can DST">
<stringSelect customId="can-dst" placeholder="Can DST" minValues={1} maxValues={1}>
<option label="Yes" value="yes" />
<option label="No" value="no" />
</stringSelect>
</label>
<label label="Can BR">
<stringSelect customId="can-br" placeholder="Can BR" minValues={1} maxValues={1}>
<option label="Yes" value="yes" />
<option label="No" value="no" />
</stringSelect>
</label>
<label label="Can SMB">
<stringSelect customId="can-smb" placeholder="Can SMB" minValues={1} maxValues={1}>
<option label="Yes" value="yes" />
<option label="No" value="no" />
</stringSelect>
</label>
<label label="Can Bridge">
<stringSelect customId="can-bridge" placeholder="Can Bridge" minValues={1} maxValues={1}>
<option label="Yes" value="yes" />
<option label="No" value="no" />
</stringSelect>
</label>
</modal>
);
},
},
},
router: (ctx) => {
if (ctx.custom_id === 'add-location-modal') {
// Handle modal submission
if (!interaction.isModalSubmit()) {
throw new Error('Expected a modal submit interaction for add-location-modal');
}
let locationName = ctx.interaction.data.components.find(
(comp) => isModalLabel(comp) && componentHasIdPrefix(comp.component, 'location-name'),
)?.component.value;
let locationShortName = ctx.interaction.data.components.find(
(comp) => isModalLabel(comp) && componentHasIdPrefix(comp.component, 'location-short-name'),
)?.component.value;
let structureTypeValue = ctx.interaction.data.components.find(
(comp) => isModalLabel(comp) && componentHasIdPrefix(comp.component, 'structure-type'),
)?.component as ComponentInteractionSelectMenuData;
let structureType = structureTypeValue ? (structureTypeValue.values[0] as StructureType) : undefined;
if (locationName && locationShortName && structureType) {
getDB().addLocation({
name: locationName,
short_name: locationShortName,
structure_type: structureType,
});
}
return 'main';
}
if (ctx.custom_id === 'add-location') {
ctx.state.data.selected = getDB().getLocationById(ctx.interaction.data.values[0]);
return 'add-location';
}
if (ctx.custom_id === 'edit-services') {
if (!ctx.interaction.isMessageComponent()) {
throw new Error('Expected a message component interaction for edit-services');
}
const data = ctx.interaction.data as ComponentInteractionSelectMenuData;
const locationId = Number.parseInt(data.values[0]);
const location = getDB().getLocationById(locationId);
if (location) {
ctx.state.data.selected = location;
return 'edit-services';
}
}
if (ctx.custom_id === 'edit-services-modal') {
// Handle modal submission
if (!interaction.isModalSubmit()) {
throw new Error('Expected a modal submit interaction for edit-services-modal');
}
const location = ctx.state.data.selected;
if (!location) {
return 'main';
}
const getServiceValue = (serviceId: string) => {
const comp = ctx.interaction.data.components.find(
(c) => isModalLabel(c) && componentHasIdPrefix(c.component, serviceId),
)?.component as ComponentInteractionSelectMenuData;
return comp ? comp.values[0] === 'yes' : false;
};
const can_jf = getServiceValue('can-jf');
const can_dst = getServiceValue('can-dst');
const can_br = getServiceValue('can-br');
const can_smb = getServiceValue('can-smb');
const can_bridge = getServiceValue('can-bridge');
getDB().updateLocation({
...location,
can_jf,
can_dst,
can_br,
can_smb,
can_bridge,
});
}
return 'main';
},
initialPage: 'main',
ephemeral: true,
},
interaction,
commandCtx,
);
},
);

View File

@@ -0,0 +1,248 @@
import {
Constants,
type ChatInputApplicationCommandStructure,
type ComponentInteractionSelectMenuData,
} from '@projectdysnomia/dysnomia';
import { appraiseItems, type Appraisal } from '@star-kitten/lib/eve/third-party/janice.js';
import {
componentHasIdPrefix,
isModalLabel,
isModalSelect,
isModalTextInput,
} from '@star-kitten/lib/discord/components';
import type { CommandContext, ExecutableInteraction } from '@star-kitten/lib/discord';
import { PageType, usePages } from '@star-kitten/lib/discord/pages';
import { serve } from 'bun';
// import { renderAppraisal } from './renderAppraisal';
// import { renderAppraisalModal } from './renderAppraisalModal';
const definition: ChatInputApplicationCommandStructure = {
type: Constants.ApplicationCommandTypes.CHAT_INPUT,
name: 'quote',
nameLocalizations: {
de: 'angebot',
'es-ES': 'cotización',
fr: 'devis',
ja: '見積もり',
ko: '견적',
ru: 'цитата',
'zh-CN': '报价',
},
description: 'Get a quote for moving your items',
descriptionLocalizations: {
de: 'Holen Sie Sie sich ein Angebot für den Umzug Ihrer Gegenstände',
'es-ES': 'Obtén una cotización para mover tus artículos',
fr: 'Obtenez un devis pour déplacer vos articles',
ja: 'アイテムを移動するための見積もりを取得します',
ko: '항목을 이동하기 위한 견적 받기',
ru: 'Получите предложение по перемещению ваших предметов',
'zh-CN': '获取移动您的物品的报价',
},
};
interface QuouteState {
serviceType: RouteType;
originId?: number;
destinationId?: number;
items?: string;
appraisal?: Appraisal;
}
// Hardcoded routes for now
interface RouteType {
id: number;
short: string;
label: string;
}
const routeTypes: Record<string, RouteType> = {
JF: { id: 1, short: 'JF', label: 'Jump Freighter' },
DST: { id: 2, short: 'DST', label: 'Deep Space Transport' },
SMB: { id: 3, short: 'SMB', label: 'Ship Maintenance Bay' },
BR: { id: 4, short: 'BR', label: 'Blockade Runner' },
CUSTOM: { id: 5, short: 'CUSTOM', label: 'Custom' },
};
interface Location {
id: number;
name: string;
supported_types: number[]; // RouteType IDs
}
const locations: Location[] = [
{ id: 1, name: 'Jita 4-4', supported_types: [1, 2, 3, 4, 5] },
{ id: 2, name: 'B-9', supported_types: [1, 2] },
{ id: 3, name: '3T7', supported_types: [1, 2, 3, 4, 5] },
{ id: 4, name: '4-H', supported_types: [1, 2] },
{ id: 5, name: 'Odebeinn', supported_types: [1, 2, 3, 4, 5] },
];
interface Route {
origin: number;
destination: number;
type: number; // RouteType ID
}
const routes: Route[] = [
{ origin: 1, destination: 2, type: routeTypes.JF.id },
{ origin: 2, destination: 1, type: routeTypes.JF.id },
{ origin: 2, destination: 3, type: routeTypes.JF.id },
{ origin: 3, destination: 2, type: routeTypes.JF.id },
{ origin: 3, destination: 4, type: routeTypes.JF.id },
{ origin: 4, destination: 3, type: routeTypes.JF.id },
{ origin: 4, destination: 5, type: routeTypes.JF.id },
{ origin: 5, destination: 4, type: routeTypes.JF.id },
{ origin: 5, destination: 2, type: routeTypes.JF.id },
{ origin: 2, destination: 3, type: routeTypes.JF.id },
{ origin: 3, destination: 4, type: routeTypes.JF.id },
{ origin: 4, destination: 5, type: routeTypes.JF.id },
{ origin: 5, destination: 2, type: routeTypes.JF.id },
{ origin: 3, destination: 2, type: routeTypes.DST.id },
{ origin: 2, destination: 4, type: routeTypes.SMB.id },
{ origin: 2, destination: 5, type: routeTypes.BR.id },
{ origin: 1, destination: 3, type: routeTypes.DST.id },
{ origin: 1, destination: 4, type: routeTypes.SMB.id },
{ origin: 1, destination: 5, type: routeTypes.BR.id },
{ origin: 2, destination: 1, type: routeTypes.JF.id },
{ origin: 3, destination: 1, type: routeTypes.DST.id },
{ origin: 4, destination: 1, type: routeTypes.SMB.id },
{ origin: 5, destination: 1, type: routeTypes.BR.id },
];
const defaultState: QuouteState = {
serviceType: routeTypes.JF,
originId: undefined,
destinationId: undefined,
items: undefined,
appraisal: undefined,
};
function uniqueDestinationForOriginAndType(typeId: number, originId?: number) {
if (!originId) {
locations.filter((loc) => loc.supported_types.includes(typeId));
}
const filtered = routes.filter((r) => r.origin === originId && r.type === typeId);
const locSet = new Set<Location>();
filtered.forEach((route, index) => {
locSet.add(locations.find((l) => l.id === route.destination)!);
});
return Array.from(locSet);
}
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
return await usePages<QuouteState>(
{
pages: {
main: {
key: 'main',
type: PageType.MESSAGE,
render: (pageCtx) => {
console.log('Rendering main page with state:', pageCtx.state.data);
return (
<container accent={0x11cc33}>
<text>{`# Quote`}</text>
<text>{`### Service: ${pageCtx.state.data.serviceType?.label ?? ''}`}</text>
<actionRow>
{Object.keys(routeTypes).map((key) => (
<button
customId={`type-${key}`}
label={routeTypes[key].short}
style={Constants.ButtonStyles.SECONDARY}
/>
))}
</actionRow>
<text>{`### Origin: ${locations[pageCtx.state.data.originId - 1]?.name ?? ''}`}</text>
<actionRow>
<stringSelect customId="route-origin" placeholder="Select Origin">
{locations
.filter((loc) => loc.supported_types.includes(pageCtx.state.data.serviceType?.id ?? -1))
.map((loc) => {
return <option label={loc?.name ?? ''} value={loc?.id.toString() ?? ''} />;
})}
</stringSelect>
</actionRow>
<text>{`### Destination: ${locations[pageCtx.state.data.destinationId - 1]?.name ?? ''}`}</text>
<actionRow>
<stringSelect customId="route-destination" placeholder="Select Destination">
<option label="Select a destination" value="1" />
</stringSelect>
</actionRow>
<text>{`### Items:\n${pageCtx.state.data.items ?? ''}`}</text>
<actionRow>
<button customId="addItems" label="Add Items" style={Constants.ButtonStyles.PRIMARY} />
</actionRow>
</container>
);
},
},
addItems: {
key: 'add-items',
type: PageType.MODAL,
render: () => {
return (
<modal title="Add Items" customId="add-items-modal">
<label
label="Items"
description="Discord limits input to 4000 characters. Add more items by submitting multiple times."
>
<textInput
customId="items-input"
placeholder={`e.g. Tritanium 22222
Pyerite 8000
Mexallon 2444`}
isParagraph={true}
required
/>
</label>
</modal>
);
},
},
},
initialPage: 'main',
initialStateData: defaultState,
ephemeral: true,
router: (pageCtx) => {
if (pageCtx.custom_id.startsWith('type-')) {
const key = pageCtx.custom_id.replace('type-', '');
pageCtx.state.data.serviceType = routeTypes[key];
return 'main';
}
if (pageCtx.custom_id === 'route-origin' && pageCtx.interaction.isMessageComponent()) {
const data = pageCtx.interaction.data as ComponentInteractionSelectMenuData;
pageCtx.state.data.originId = Number.parseInt(data.values[0]);
return 'main';
}
if (pageCtx.custom_id === 'route-destination' && pageCtx.interaction.isMessageComponent()) {
const data = pageCtx.interaction.data as ComponentInteractionSelectMenuData;
pageCtx.state.data.destinationId = Number.parseInt(data.values[0]);
return 'main';
}
if (pageCtx.custom_id === 'addItems') {
return 'addItems';
}
if (pageCtx.custom_id === 'add-items-modal' && pageCtx.interaction.isModalSubmit()) {
let items = '';
pageCtx.interaction.data.components.forEach((comp) => {
if (isModalLabel(comp)) {
if (isModalTextInput(comp.component) && componentHasIdPrefix(comp.component, 'items-input')) {
items = comp.component.value || items;
}
}
});
pageCtx.state.data.items = pageCtx.state.data.items ? `${pageCtx.state.data.items}\n${items}` : items;
}
return 'main';
},
},
interaction,
ctx,
);
}
export default {
definition,
execute,
};

View File

@@ -0,0 +1,77 @@
import {
type ExecutableInteraction,
type CommandContext,
Locale,
type ChatCommandDefinition,
} from '@star-kitten/lib/discord';
const definition: ChatCommandDefinition = {
name: 'time',
nameLocalizations: {
[Locale.DE]: 'zeit',
[Locale.ES_ES]: 'hora',
[Locale.FR]: 'heure',
[Locale.JA]: '時間',
[Locale.KO]: '시간',
[Locale.RU]: 'время',
[Locale.ZH_CN]: '时间',
},
description: 'Get the current EVE time',
descriptionLocalizations: {
[Locale.DE]: 'Holen Sie sich die aktuelle EVE-Zeit',
[Locale.ES_ES]: 'Obtén la hora actual de EVE',
[Locale.FR]: "Obtenez l'heure actuelle d'EVE",
[Locale.JA]: '現在のEVE時間を取得します',
[Locale.KO]: '현재 EVE 시간을 가져옵니다',
[Locale.RU]: 'Получите текущее время EVE',
[Locale.ZH_CN]: '获取当前的EVE时间',
},
};
const eveTimeText = {
[Locale.EN_US]: 'EVE Time',
[Locale.EN_GB]: 'EVE Time',
[Locale.DE]: 'EVE-Zeit',
[Locale.ES_ES]: 'Hora EVE',
[Locale.FR]: "Heure d'EVE",
[Locale.JA]: 'EVE時間',
[Locale.KO]: 'EVE 시간',
[Locale.RU]: 'Время EVE',
[Locale.ZH_CN]: 'EVE时间',
};
function jsx(component: any) {
return {
flags: 2,
components: [component],
};
}
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
if (!interaction.isApplicationCommand()) return;
const now = new Date();
const eveTime = now.toISOString().split('T')[1].split('.')[0];
const eveDate = now.toLocaleDateString(interaction.locale, {
timeZone: 'UTC',
year: 'numeric',
month: 'long',
day: '2-digit',
weekday: 'long',
});
interaction.createJSXMessage(
<container>
<text>
{`### ${eveTimeText[interaction.locale] || eveTimeText[Locale.EN_US]}
${eveTime}
${eveDate}`}
</text>
</container>,
);
}
export default {
definition,
execute,
};

View File

@@ -0,0 +1,60 @@
import { Database } from 'bun:sqlite';
import locationTables, * as locationQueries from './location';
let db: Database = undefined;
const queries = {
...locationQueries,
};
function createTables() {
locationTables.createTable(db!);
}
function dropTables() {
locationTables.dropTable(db!);
}
function close() {
if (db) {
db.close();
db = null;
}
}
function initializeDatabase(dbPath: string = process.env.CONCIERGE_DB_PATH || ':memory:') {
db = new Database(dbPath);
createTables();
Object.keys(queries).forEach((key) => {
if (typeof queries[key] === 'function') {
queries[key] = queries[key].bind(null, db);
}
});
}
type OmitFirstArg<F> = F extends (arg1: any, ...args: infer R) => infer Ret ? (...args: R) => Ret : never;
type CurriedObject<T, O> = {
[K in keyof T]: T[K] extends (arg1: O, ...args: infer R) => infer Ret ? OmitFirstArg<T[K]> : T[K];
};
export type DB = CurriedObject<Omit<typeof queries, 'default'>, Database> & {
db: Database;
createTables: () => void;
dropTables: () => void;
close: () => void;
};
export function getDB(): DB {
if (!db) {
initializeDatabase();
}
return {
...(queries as any),
default: undefined,
createTables,
dropTables,
close,
db,
} as DB;
}

View File

@@ -0,0 +1,103 @@
import type { Database } from 'bun:sqlite';
const TABLE_NAME = 'locations';
export enum StructureType {
NPC = 'NPC',
Keepstar = 'Keepstar',
Fortizar = 'Fortizar',
Astrahus = 'Astrahus',
Sotiyo = 'Sotiyo',
Azbel = 'Azbel',
Raitaru = 'Raitaru',
Athanor = 'Athanor',
Tatara = 'Tatara',
}
export interface Location {
location_id: number;
name: string;
short_name: string;
structure_type: StructureType;
can_jf?: boolean;
can_dst?: boolean;
can_br?: boolean;
can_smb?: boolean;
can_bridge?: boolean;
}
export default {
createTable: (db: Database) => {
db.run(
`CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
location_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
short_name TEXT NOT NULL,
structure_type TEXT NOT NULL,
can_jf BOOLEAN NOT NULL DEFAULT 0,
can_dst BOOLEAN NOT NULL DEFAULT 0,
can_br BOOLEAN NOT NULL DEFAULT 0,
can_smb BOOLEAN NOT NULL DEFAULT 0,
can_bridge BOOLEAN NOT NULL DEFAULT 0
)`,
);
},
dropTable: (db: Database) => {
db.run(`DROP TABLE IF EXISTS ${TABLE_NAME}`);
},
};
export function addLocation(db: Database, location: Omit<Location, 'location_id'>) {
const stmt = db.prepare(
`INSERT INTO ${TABLE_NAME} (name, short_name, structure_type, can_jf, can_dst, can_br, can_smb, can_bridge)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
);
const result = stmt.run(
location.name,
location.short_name,
location.structure_type,
location.can_jf ? 1 : 0,
location.can_dst ? 1 : 0,
location.can_br ? 1 : 0,
location.can_smb ? 1 : 0,
location.can_bridge ? 1 : 0,
);
return result.lastInsertRowid as number;
}
export function updateLocation(db: Database, location: Location) {
const stmt = db.prepare(
`UPDATE ${TABLE_NAME}
SET name = ?, short_name = ?, structure_type = ?, can_jf = ?, can_dst = ?, can_br = ?, can_smb = ?, can_bridge = ?
WHERE location_id = ?`,
);
stmt.run(
location.name,
location.short_name,
location.structure_type,
location.can_jf ? 1 : 0,
location.can_dst ? 1 : 0,
location.can_br ? 1 : 0,
location.can_smb ? 1 : 0,
location.can_bridge ? 1 : 0,
location.location_id,
);
}
export function getLocationById(db: Database, locationId: number): Location | null {
const stmt = db.prepare(`SELECT * FROM ${TABLE_NAME} WHERE location_id = ?`);
const row = stmt.get(locationId) as Location | undefined;
return row || null;
}
export function getAllLocations(db: Database): Location[] {
const stmt = db.prepare(`SELECT * FROM ${TABLE_NAME}`);
const rows = stmt.all() as Location[];
return rows;
}
export function deleteLocation(db: Database, locationId: number) {
const stmt = db.prepare(`DELETE FROM ${TABLE_NAME} WHERE location_id = ?`);
stmt.run(locationId);
}

View File

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

View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"jsx": "react-jsx",
"jsxImportSource": "@star-kitten/lib/discord",
"moduleResolution": "bundler",
"paths": {
"@/*": ["./src/*"]
},
"typeRoots": ["src/types", "./node_modules/@types"]
},
"references": [{ "path": "../lib" }],
"include": ["src", "types"],
"exclude": ["node_modules", "dist", "build", "**/*.test.ts"]
}