Initial commit
This commit is contained in:
33
packages/concierge-bot/.env.development
Normal file
33
packages/concierge-bot/.env.development
Normal 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=="
|
||||
23
packages/concierge-bot/.env.production
Normal file
23
packages/concierge-bot/.env.production
Normal 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"
|
||||
182
packages/concierge-bot/.gitignore copy
Normal file
182
packages/concierge-bot/.gitignore copy
Normal 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
|
||||
5
packages/concierge-bot/.prettierrc
Normal file
5
packages/concierge-bot/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
}
|
||||
8
packages/concierge-bot/.prettierrc.yaml
Normal file
8
packages/concierge-bot/.prettierrc.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
trailingComma: all
|
||||
tabWidth: 2
|
||||
useTabs: false
|
||||
semi: true
|
||||
singleQuote: true
|
||||
printWidth: 140
|
||||
experimentalTernaries: true
|
||||
quoteProps: consistent
|
||||
15
packages/concierge-bot/README.md
Normal file
15
packages/concierge-bot/README.md
Normal 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.
|
||||
4
packages/concierge-bot/data/.gitignore
vendored
Normal file
4
packages/concierge-bot/data/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
29
packages/concierge-bot/package.json
Normal file
29
packages/concierge-bot/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
220
packages/concierge-bot/src/commands/locations.command.tsx
Normal file
220
packages/concierge-bot/src/commands/locations.command.tsx
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
248
packages/concierge-bot/src/commands/quoute.command.tsx
Normal file
248
packages/concierge-bot/src/commands/quoute.command.tsx
Normal 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,
|
||||
};
|
||||
77
packages/concierge-bot/src/commands/time.command.tsx
Normal file
77
packages/concierge-bot/src/commands/time.command.tsx
Normal 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,
|
||||
};
|
||||
60
packages/concierge-bot/src/lib/db/index.ts
Normal file
60
packages/concierge-bot/src/lib/db/index.ts
Normal 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;
|
||||
}
|
||||
103
packages/concierge-bot/src/lib/db/location.ts
Normal file
103
packages/concierge-bot/src/lib/db/location.ts
Normal 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);
|
||||
}
|
||||
3
packages/concierge-bot/src/main.ts
Normal file
3
packages/concierge-bot/src/main.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { startBot } from "@star-kitten/lib/discord";
|
||||
|
||||
startBot();
|
||||
16
packages/concierge-bot/tsconfig.json
Normal file
16
packages/concierge-bot/tsconfig.json
Normal 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"]
|
||||
}
|
||||
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],
|
||||
}),
|
||||
}));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user