Initial commit

This commit is contained in:
JB
2026-02-12 21:12:11 -05:00
commit 9023b68e66
22 changed files with 1291 additions and 0 deletions

33
.env.development Normal file
View File

@@ -0,0 +1,33 @@
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY_DEVELOPMENT="02572da3d4f3a844588a944214c0e142a5a01deaa6551456af146d34b574024416"
# .env.development
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY="02292a330aa041b5f7efc51504e0c208accba67a6877a217ab43cbb59c3c0c3e66"
# .env
DEBUG="encrypted:BDJ8/K1Qm9bAcGm1D9etyuk0bQqoXQU3nidRnJPz5gTF9xPMhaPpBQ3hOmLKZBOJoKnydVRz2XFywq0W6S8bo6pC1dV6gNYGQMcD88fIVNF9Mi2zE4L1B0AjQC/h/QgQ58ihdRc="
PORT="encrypted:BN/VGuk3dAZsQKKjRiKTQpWJtgRrtVYNYXr3CahoOUIYUvrVwIS8ATXpKqDwUZoyEuLv5Ypgg4FVkPWiJ8EP/jpG3mcHY58gFYrNOyPVBSbBt0gprJLsQtZ8hzoYCg0OyrhVnoY="
NODE_ENV="encrypted:BF8jXFkdSedzxHjyIixwWxowwHLqEBKxv8xPNYY6HquRAL70hd7Z43hsLTX9qSa+VINVUkZhKt+swYJ9GYI0RsGXDGNlOofTtdxMmtOLIjsd3OVZbQ89ycW0LES1/x2rPsRoZcBi8CwuUYvp"
LOG_LEVEL="encrypted:BCnKxBPtZLil3M8cexphmxIV7ods5JopnzMee2pH4MD+t1XTxRMPL2gWdrdHYssSrClQxUz6uX5wnKouhJ8i5WqDa3yczz9qv1ITtV9KQhYEdU0vRQ22IU8+y2BMQF257vNJIZg4"
BASE_URL="encrypted:BKc9lOSo7zFrkUcH4qEr2QGcVKbXAVB0mvnkdSvzNxW8JFpfOGUdToxMOmSngSxEOlJ4awPiaFLkeXEY/iGWZYTCmd3OiHHr8ZYdwpesh5OqYxdeJ1kD3WT/nihjUFvMcaVRmMFw6Mg4q0TvOaXq95Rh1na39eteLicv"
EVE_CLIENT_ID="encrypted:BFHLubJYNIBg7wzfvWxe8sElN4MglmxHSbzuPls8/F25GwPTPBovlrZNNhGAIidTlm05fRh3ciFYFa//i6KgB2kidqB14nPMU899vS49SAZpL7KBTuCkWiURjAFxhPcgG0j2546SUxxIRYzb8lC9/0p7+Sj3JTeEvm/7vDxZ9NOf"
EVE_CLIENT_SECRET="encrypted:BAX72LsKPglufcVruKveB5cbMFRImFa7TKEjPTaSbzkAERkDdV10fJ4HsBTWS9crTM5Kp14A8ghg8FAjnWaozgH3re3eiz+/BaFT+EiOY8C8LCSKyulEqZH0sbA1NitaTRk6dX3QUoIcrCXJMTlJVgFnYkKikCdHNy2DPbVktY0Fg4+y1oZcklY="
EVE_CALLBACK_URL="encrypted:BHvXeR5TFcpYzwyySIi79OMljtILmgPObAsn4Wbr89hdoRgrS65kq71XFTGb4ZmKpaFVIwyodZYAMQCRZ+sFFKyPlA6jopxOw3Pxb6epjtXxtQSYpYIrsgczFBg+XuWODOzxcd4HGcGzibIIPB5DE0MLm/OkD0An+KyFoHLhuA1Kb53c7NEGHeobb5p0"
ESI_USER_AGENT="encrypted:BGHEVuvVn2YLjnFTjHzPBt/PWc+/eNIEPC5zs6BvlRB3lganokRPY4U0749mW4ZEvSQVm/QAA43wstL+gvtjkog0WnkCEifjCrUgv40flhJEGb3Y/BeMt7StTDEamUAIgdso7sdDuRZE/EwSTf3fuVlu1jJWKuYs/G8Z9U/DL1KqBKV8AUz0paPz+AdFlJx0tQE5IRZdB1DVtOgvPTVzrzXKD9JqMNRKPkIXy7Puib0="
DISCORD_APP_ID="encrypted:BOdRJjYl0STTWJsvJr72WYTVPrhS0kwrSgE7xdZkrK2eNsQ2yloy2MmpUBgZNSbtI0R+lbX7tcTROoCgnwQIakGzBjmD9MD5HsrNDuJjSqmNt9m14MVnSwRlS3qk4S8/f6q2sFS2S+WYtWQ/7gIXKAAWRAU="
DISCORD_APP_SECRET="encrypted:BDC8e7tOq0KaFQ/L/7rQuDcxe9S6IMQkXTliJVTvDqBsUS45ogpYY8LMPJR2c4IectLUFPgeG1JFcgVLGYLR5wm134DIqargWXkD4Fu8yjscoNaX/8yGgjWATJDcpoByBB+OichgRe+2Fl2UyuEEOm3+WrrXt3PSJTAd1uESGYkd"
DISCORD_PUBLIC_KEY="encrypted:BIBoYvkwIo+o+YaXgi713zvnS0whOwO3BLcesMEOqgB1h59YRgOB96g+geNtbX31u2RUf8su3fgo/zDY2xz+yNzk0UdEPKBT1N7kQlrUiMRyWhJBwL44Jgy/cUmjcq4VlYeYUe7pMxOeUILIkEzNzIWy/3QAwBKtQv0OnAqWBlkRNaP8xQvtDbQOTAhj655e919vuGwSaUT5EMBVW6MEKSg="
DISCORD_BOT_TOKEN="encrypted:BFjKo3ysXlVOkddHVpRC5c+4ICQTDQftROckpQ/icS233VldpcPM1oRE7Qzh9P872gA5kUpHG4QH1A3TMGNcA1ESTEIDEf7Z9/H0dRxmEVINo9PkfXyDLa334LAXXj4EzaeMoikNyoDBiWdaH7YWVVbLdah8o+8DEf1sGPkngfsf5gdx4/+ht7a5O9VdNInTw+0DiqPyPhCaZIvbfoHdyjwrPwmWpG5i5Q=="
DISCORD_TEST_GUILD_ID="encrypted:BHC82XO6i5pmt8VPuOlz/1M8vSmQMP5EElZIiooRLx1Dbb5CR9XGyvopk/PLLhtFKz43g8PCTCj0436fZErs7EhVGFM9e2besr5Ncyg/zXm6+GJ8LMRr0rh+R6jSMJswXC1ZC4eYYW/KWO9RO0/5b1MRpw=="
JANICE_KEY="encrypted:BL/i1VVvrx++M36yvTjVxiwwxK56bDb3MvZnU8jo6aI9v7wcXM4x4UbvvIopXjvbZnDbwbBMQQDTQm1dvrGVicqmzeExcL3BwvQEmSJqitArxo0cDcLBai4HxaotdbCCtQ2HY4dXrsPU7VSK/5hDolCKjVXrdBjZdW4pl9IlFdtS"
PERPLEXITY_API_KEY="encrypted:BAen20ADUNSpGeJsU1fiH+UfLZJPG5rLNlT6GnkLwZNoITM3PyPFGzY1IewrBtP/jST0y8XYB3sR5VZ2NlZ/ksPs102oRw3xq8RVtmPPfQ0hwcc1dCdRs9CStjW0ecpP4DSKImzMBGkzXptLzyY+L8yTrUNA6hpWWRZVb3d2Qc+DoJs43SlLHg2WF5PQ5zm1GDFGY4XX"
STAR_KITTEN_KV_DB_PATH="encrypted:BBVO77V74cqJCRZnRfRBPCxwEzHB2y0Zlu0MYZWOvta7WXTUrjf+2R6OqRGAr1ufivjlxcAnI7pd+C1Z5p3Kso/o8JLW7tLtfIp8qYBnI6GvwHNd2n64A7jyrRY9q3QgGeFrJwC7ri9WfMVB8Hg4"
POSTGRES_URL="encrypted:BDDpo6rOefNrh98gSuNRBadPmKkSz0WhOauh0Yc/Rh2VgbnRCeDWsb2Ps7FjwtTbUedapBCiNNX9P43voCVWyYAqWxWQUJYByI4LEgPb39VkXlcXkaToytDSTmKOxgHZPIGkSkG+5GxU8rFxBVe4gbT6P9wVnV8E6SX/0YtdFs7NrbLUrYRstomI8mMR3yTrWnBe2TwlRkZSe2b41Zc3Pl4rabIFaeZhyjqmZkksrvW8d+vU0k/ZOee+updl"
NEO4J_URL="encrypted:BFZGXaev/GzYvKfFGOXR08jhe42qIPLG6VDbuWADT7ulH2i80lvG9j79kTQoI+XQhyiZqG5mr0w8N8/WGyutCvVLDOxRChPVV+XfuXoYCihWiR8apO7z+8qClxloAvfsSel4DssmP1lFvKoyjgmqfjd1Ks3y4FLHBQrt9HlHGpQEFTTWMQuVn/vwg38IYMgjGKXAc9puzP7vyGhmfUbpmI9dwJPox5jKue69DifecHw="

23
.env.production Normal file
View File

@@ -0,0 +1,23 @@
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/ public-key encryption for .env files /
#/ [how it works](https://dotenvx.com/encryption) /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY_PRODUCTION="02f0469506f6722d8fcc179c199ff159ca32f082000c8e7a1465891adb50a4c031"
# .env.production
DEBUG="encrypted:BJ7+kItX/nGaAYi54Ovt0fI9uWkl7AaIc6MXfSFNkHOkjAYH6UsTJvRWfNIsCN6Jd1F458BcNXQIEv4BS/PMEq7hdtqd5KHZH2aIlPlonmetBD3w7sNhm+mFxrCLSnQK8Oam3SJ6"
PORT="encrypted:BAro5r3E/TSgYas47yFGjSVCWj3xW1myqtBIK6AA49b9E8I2H1b1UTtlYwdS9w64ylRuavk1YpxGeQWBJsaMVpsnhMJzADL/V3mHFpkL7MzXvyklS1BGyXPWzgHkjoikS+wnCd8="
NODE_ENV="encrypted:BEfswbi+NKMs0ZwOIalYRDm8RPRAh0Y8WxgbfUw6eMU8rw9LtOpIvMIHhn9uV7iqcRMx6Ku4hXEtT+Kk2HEr/Yk7JF0m2gUJMaZFEaOlw/oFp6W70pNAIw2bLo+ZrHplsd4RSCz+j/BuYh4="
LOG_LEVEL="encrypted:BPNoOdNxMMESi9WfCFO9aFQLp4H7MoJcvwh0DRexiL8/38Rj2h8+fp9apnPv5fFPZDJFAOA6CsQI5deVMIXYwcCnqqGcVNX9UHpATH8JmpXxg1K5D9uB9hrn6u8CVnaunpq6E6w="
BASE_URL="encrypted:BHqtFdukWFfbuh8fog7fE2D42jOlgcb9tTySZwBa9SWA5Z3+XLvvJq/VPox1gJTh/ouP4txRYlpTt6lv5C3NLzZzhfmOSjgkJ/2p6yZXKTmDL2uNRSFyVtorb9UsGn1C80hsxr1MWivbBJ8OuQQwZAVQEsERHBZL"
DISCORD_APP_ID="encrypted:BC8cpOQW8JvbEgG50vfoZ9qqHlSxvn0Yfw+1actEv83cd7of+R5r/TlZYwfwqeIOnyUyllH/qi7w2RQr3Ll+L4Z7EL7e2P1rkHqKgGOs5iynaVCrFh2nS2mWFQQtGa0WmVhXNUIOV8jx65zqOMhJ5/3GyFs="
DISCORD_APP_SECRET="encrypted:BD4cRqwdkXfWlITxbomEbVJecHvSTvCl2s1K25dxsUltTchdG+XDwNG8FgqpOwIrNkJgd3g3FuPRw7YTul6idnh6Sk83k0HSKQfRhAK1Ch/52Er4aD+DPzQxfK2jNSrBlI+QhNlqKTrSQmZws8Np57sfsKhjdr2hwyPN99MDfBA+"
DISCORD_PUBLIC_KEY="encrypted:BCXkhk8GO8oml62ribVt21KZzj345URPDbyHS+WuJLc/1hO0pBEIMts7nUAgJJn4uk9nJLzkEtmgZ+9ZXn7ujIuyao+djAYKltgmK5KJoDeFiXzK1q6x1jnJQL7wf0EVY7+/4MIWZPs18uYv2wjuHpvyg0Vxo/vc+rpZsQHIAUqxE6hCcTxEYPF9pzTi/75bQcvswuOnz9a5o+GfRodcnVk="
DISCORD_BOT_TOKEN="encrypted:BCGnL0T+6rwwq7wEFIv54A/npnnZbeGYnqIYmvdETPyHXfceTI3S2tBqHLaug0r3JdqVTTUtWsI+JlXFRw7qV/bWJ1nEFU7C9z3Q7N1vNz6CWuVpySJcS+0/7y2JYuy98uWKnMPQUVrX8E68UX4mt2J/0haTogBSbntJhoIZzGPjELbKoRtNPt8bzuHgrjzaspPWeoDF+MnCxnw0D3YhFffoqEMSeYuEzA=="
EVE_CLIENT_ID="encrypted:BIdohc0Ho58IPhM1V/lUYe//ksqpv6WRVbjTQUN6YPPHq4tk9+sPTJFYwH3BcA47nj2HRQN5LAH2+YVKGz5qhqLaQihIhGzycyQJxmidewYq6Ov4ernE82MeQwSTxZS7zaa1VESkE9t6zh8vW4OllJ1Oz+iv5KfqZ+WeGGmNUIrd"
EVE_CLIENT_SECRET="encrypted:BLy/RFWr4FYiFFXjkap97EKnoJB7f1hGWxW03nEgLrrWBc+eCVO4L915keUp9w6bC34bwUkD1Xwx2fGFM8TNykLYcnxFlgh8dTRjTrJBTuxZZ4rcIqQAiIaW9pkI02S34fhoXdeDT3rAF74l3PlZSXWVJ3YhdxD8hyOnORK9ckxLfvGA0acrqAU="
EVE_CALLBACK_URL="encrypted:BA8NpXsxrafYFRaEgZ+M7t5a2U6TQMh2D7HNfCcCvVhYDK4eagXnaUkwGVMaUMjef2yOmNreCWk9yT7Kpmc+zUsaGfz9my7CbkuNXUMqLcx0PqIlIlGUlewc2ckb0Xm0Y62GadbkoDSwzM39G6xqwd71vIKQq35TcxgHe0SyJi/RGy6XAn/k/JXa"
ESI_USER_AGENT="encrypted:BEgeNZzEAtym7QUMfEX+I+OtPTQZo2hXk2wPGSScluPUMrwzC6ewCGRa5zPkQ7jG9SwGLTkEfYn5PvBg3bwTEw5EEnzguOdCHoDEa1lCzTSgWwSVIWDbRBnwsbLTVfhJZ54KM/pJItW5LNsQasoWuh9pCnbQAnRgs3MIYwGo1lIAs2bvkbdf6AbiBEHBnGXsADwVEINyJx/K6cRQlcSMB+87QMbnjkXuqWgAm9r3bbdlDvpdbNikAbU="
AUTH_DB_PATH="encrypted:BAAhDiyG8vHhnKJX8o7g3kSziMwImr5PqjVf4K6P0LnWxaqYSvHlexMTKMtd17mmk5GJ3jZOEp3L9iWqhWZuhb9wL/HhrkixyATwQwJfa+hHBZCJFOFiC55/zbXT+mJKNFqvMcs39YFCzPgrNqOEiwwAGop0Mlco"
JANICE_KEY="encrypted:BAS4U3jmYPhGjuaxFX5ZZYvRp7zHuoFwriTK4y36SoZp/LOLvZAIiZu07QNMEbNTmEIR4RgEyS1g6ztO41yHNZUgaB5yZa0/fSTnsZv0boVMABmX8y3z3QlXE2PqORkWM2vFsllkL1F8Czyt3cJw7CTLvvvXQHJOV4iPj9Oo6dFb"
PERPLEXITY_API_KEY="encrypted:BOh0av8o7bYMRBmbyvx63oBOeMee4ceMXRI6lBRenvZnDgI8AfNq6DmPWJ+upb+gat/FADqQWqKobclIDRbBTfMa3RlhehHtjtQnVQl4zqecAQ/Yv4bisanhUO9I7h2zyf+3BOx5vE5+OnI+gOHZ1Kru5Gal1DVXQrxMgjrscVW1SLxrWBTs5Nn0cT8evqb2z4X398um"

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
dist
node_modules
.env.keys
packages/**/coverage
*.tsbuildinfo
.vscode/settings.json
db/
data/

2
.husky/pre-commit Normal file
View File

@@ -0,0 +1,2 @@
bun run encrypt
git add .env.development .env.production

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
@star-kitten:registry=https://git.f302.me/api/packages/jb/npm/

8
.prettierrc.yaml Normal file
View File

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

67
README.md Normal file
View File

@@ -0,0 +1,67 @@
# Star Kitten Discord Bot
A Discord bot for [EVE Online](https://www.eveonline.com/) built with [bun](https://bun.sh/) and [@star-kitten/lib](https://git.f302.me/jb/star-kitten)
# [Install Star Kitten](https://discord.com/oauth2/authorize?client_id=1288711114388930601)
## Running the Bot
This bot runs on [Bun](https://bun.sh/)! To install Bun, run one of the following commands.
_Linux & MacOS_
```bash
curl -fsSL https://bun.sh/install | bash
```
_Windows_
```bash
powershell -c "irm bun.sh/install.ps1 | iex"
```
---
### Install bot dependencies
```bash
bun install
```
### Download static eve reference data & Hoboleaks archive from [EVE Ref](https://everef.net/)
```bash
bun get-data
```
### Run the bot
```bash
bun dev
```
## Environment Variables
Create a .env file in the root directory with the following values:
```yaml
# Discord - https://discord.com/developers/applications
DISCORD_BOT_TOKEN=YOUR_BOT_TOKEN
#General
BASE_URL=http://localhost:3000
DEBUG=true
PORT=3000
NODE_ENV=development
LOG_LEVEL=debug
# EVE - https://developers.eveonline.com/applications
EVE_CLIENT_ID=YOUR_EVE_CLIENT_ID
EVE_CLIENT_SECRET=YOUR_EVE_SECRET
EVE_CALLBACK_URL=http://localhost:3000/auth/callback
ESI_USER_AGENT=ADD_YOUR_USER_AGENT_INFO_HERE
# For using Janice's Appraisal API
JANICE_KEY=XXX
```

142
bun.lock Normal file
View File

@@ -0,0 +1,142 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "star-kitten",
"dependencies": {
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
"@star-kitten/discord": "link:@star-kitten/discord",
"@star-kitten/eve": "link:@star-kitten/eve",
"@star-kitten/eve-discord": "link:@star-kitten/eve-discord",
"@star-kitten/util": "link:@star-kitten/util",
},
"devDependencies": {
"@dotenvx/dotenvx": "^1.49.0",
"@types/bun": "^1.2.21",
"@types/node": "^24.3.1",
"husky": "^9.1.7",
"mkdirp": "^3.0.1",
"prettier": "^3.6.2",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.49.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-M1cyP6YstFQCjih54SAxCqHLMMi8QqV8tenpgGE48RTXWD7vfMYJiw/6xcCDpS2h28AcLpTsFCZA863Ge9yxzA=="],
"@ecies/ciphers": ["@ecies/ciphers@0.2.4", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w=="],
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@projectdysnomia/dysnomia": ["@projectdysnomia/dysnomia@github:projectdysnomia/dysnomia#5e3300e", { "dependencies": { "ws": "^8.18.0" }, "optionalDependencies": { "@stablelib/xchacha20poly1305": "~1.0.1", "opusscript": "^0.1.1" }, "peerDependencies": { "@discordjs/opus": "^0.9.0", "erlpack": "github:discord/erlpack", "eventemitter3": "^5.0.1", "pako": "^2.1.0", "sodium-native": "^4.1.1", "zlib-sync": "^0.1.9" }, "optionalPeers": ["@discordjs/opus", "erlpack", "eventemitter3", "pako", "sodium-native", "zlib-sync"] }, "projectdysnomia-dysnomia-5e3300e"],
"@stablelib/aead": ["@stablelib/aead@1.0.1", "", {}, "sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg=="],
"@stablelib/binary": ["@stablelib/binary@1.0.1", "", { "dependencies": { "@stablelib/int": "^1.0.1" } }, "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q=="],
"@stablelib/chacha": ["@stablelib/chacha@1.0.1", "", { "dependencies": { "@stablelib/binary": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg=="],
"@stablelib/chacha20poly1305": ["@stablelib/chacha20poly1305@1.0.1", "", { "dependencies": { "@stablelib/aead": "^1.0.1", "@stablelib/binary": "^1.0.1", "@stablelib/chacha": "^1.0.1", "@stablelib/constant-time": "^1.0.1", "@stablelib/poly1305": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA=="],
"@stablelib/constant-time": ["@stablelib/constant-time@1.0.1", "", {}, "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg=="],
"@stablelib/int": ["@stablelib/int@1.0.1", "", {}, "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w=="],
"@stablelib/poly1305": ["@stablelib/poly1305@1.0.1", "", { "dependencies": { "@stablelib/constant-time": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA=="],
"@stablelib/wipe": ["@stablelib/wipe@1.0.1", "", {}, "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg=="],
"@stablelib/xchacha20": ["@stablelib/xchacha20@1.0.1", "", { "dependencies": { "@stablelib/binary": "^1.0.1", "@stablelib/chacha": "^1.0.1", "@stablelib/wipe": "^1.0.1" } }, "sha512-1YkiZnFF4veUwBVhDnDYwo6EHeKzQK4FnLiO7ezCl/zu64uG0bCCAUROJaBkaLH+5BEsO3W7BTXTguMbSLlWSw=="],
"@stablelib/xchacha20poly1305": ["@stablelib/xchacha20poly1305@1.0.1", "", { "dependencies": { "@stablelib/aead": "^1.0.1", "@stablelib/chacha20poly1305": "^1.0.1", "@stablelib/constant-time": "^1.0.1", "@stablelib/wipe": "^1.0.1", "@stablelib/xchacha20": "^1.0.1" } }, "sha512-B1Abj0sMJ8h3HNmGnJ7vHBrAvxuNka6cJJoZ1ILN7iuacXp7sUYcgOVEOTLWj+rtQMpspY9tXSCRLPmN1mQNWg=="],
"@star-kitten/discord": ["@star-kitten/discord@link:@star-kitten/discord", {}],
"@star-kitten/eve": ["@star-kitten/eve@link:@star-kitten/eve", {}],
"@star-kitten/eve-discord": ["@star-kitten/eve-discord@link:@star-kitten/eve-discord", {}],
"@star-kitten/util": ["@star-kitten/util@link:@star-kitten/util", {}],
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"dotenv": ["dotenv@17.2.2", "", {}, "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q=="],
"eciesjs": ["eciesjs@0.4.15", "", { "dependencies": { "@ecies/ciphers": "^0.2.3", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0" } }, "sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"opusscript": ["opusscript@0.1.1", "", {}, "sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
}
}

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "star-kitten",
"version": "0.0.1",
"description": "A Discord bot for Eve Online",
"author": "j-b-3",
"type": "module",
"module": "src/main.ts",
"private": true,
"peerDependencies": {
"typescript": "^5"
},
"devDependencies": {
"@dotenvx/dotenvx": "^1.49.0",
"@types/bun": "^1.2.21",
"@types/node": "^24.3.1",
"husky": "^9.1.7",
"mkdirp": "^3.0.1",
"prettier": "^3.6.2"
},
"dependencies": {
"@projectdysnomia/dysnomia": "github:projectdysnomia/dysnomia#dev",
"@star-kitten/util": "link:@star-kitten/util",
"@star-kitten/discord": "link:@star-kitten/discord",
"@star-kitten/eve": "link:@star-kitten/eve",
"@star-kitten/eve-discord": "link:@star-kitten/eve-discord"
},
"scripts": {
"dev": "bunx @dotenvx/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",
"prepare": "husky"
}
}

5
src/commands/index.ts Normal file
View File

@@ -0,0 +1,5 @@
import '@star-kitten/eve-discord/commands/time.command.js';
import '@star-kitten/eve-discord/commands/time-from-now.command.js';
import '@star-kitten/eve-discord/commands/appraise.command.js';
import './search/search.command';

View File

@@ -0,0 +1,98 @@
import { ButtonStyle, ChatInputCommandInteraction, CommandInteraction, MessageFlags } from 'discord.js';
import { typeSearch } from './search';
import { createActionRow, useNavigation, type ResumeableInteraction } from '@lib/discord';
import { getTypeBlueprints, getTypeSchematics, getTypeSkills, getTypeVariants, typeHasAttributes, type Type } from 'star-kitten-lib/eve';
import { mainPage, attributesPage, fittingPage, skillsPage, industryPage } from './pages';
export enum PageKey {
MAIN = 'main',
ATTRIBUTES = 'attributes',
FITTING = 'fitting',
SKILLS = 'skills',
INDUSTRY = 'industry',
}
export interface TypeContext {
type: Type;
interaction: CommandInteraction;
disabled?: boolean;
buildButtonRow: (key: string, context: TypeContext) => any[];
}
export interface ItemLookupOptions {
ephemeral: boolean;
type: string;
}
export async function itemLookup(interaction: ChatInputCommandInteraction, options: ItemLookupOptions, saveResume: (messageId: string) => void) {
const deferred = await interaction.deferReply({ flags: options.ephemeral ? MessageFlags.Ephemeral : undefined });
const name = interaction.options.getString('name') ?? '';
await lookup(deferred.interaction as any, options, name, interaction.guild?.preferredLocale, saveResume);
}
export async function resumeItemLookup(interaction: ResumeableInteraction, options: ItemLookupOptions, name: string, saveResume: (messageId: string) => void) {
await lookup(interaction, options, name, interaction.guild?.preferredLocale, saveResume);
}
async function lookup(messageOrInteraction: ResumeableInteraction | ChatInputCommandInteraction, options: ItemLookupOptions, name: string, locale: string, saveResume: (messageId: string) => void) {
const type = await typeSearch(name);
if (!type) {
if (messageOrInteraction instanceof ChatInputCommandInteraction) {
messageOrInteraction.editReply({ content: `${options.type} ${name} not found` });
} else {
messageOrInteraction.message.edit({ content: `${options.type} ${name} not found` });
}
return;
}
const updateContext = async (key: string, context: TypeContext) => {
return Promise.resolve(key);
};
const buildButtonRow = (key: string, context: TypeContext) => {
return createActionRow(
{ customId: PageKey.MAIN, label: 'Main', style: ButtonStyle.Primary, disabled: key === PageKey.MAIN },
typeHasAttributes(context.type) && {
customId: PageKey.ATTRIBUTES,
label: 'Attributes',
style: ButtonStyle.Primary,
// disabled: key === PageKey.ATTRIBUTES,
},
typeHasAttributes(context.type) && {
customId: PageKey.FITTING,
label: `Fitting${getTypeVariants(context.type).length > 0 ? ' | Variants' : ''}`,
style: ButtonStyle.Primary,
// disabled: key === PageKey.FITTING,
},
getTypeSkills(context.type)?.length > 0 && {
customId: PageKey.SKILLS,
label: 'Skills',
style: ButtonStyle.Primary,
// disabled: key === PageKey.SKILLS,
},
(getTypeBlueprints(context.type)?.length > 0 || getTypeSchematics(context.type)?.length > 0) && {
customId: PageKey.INDUSTRY,
label: 'Industry',
style: ButtonStyle.Primary,
// disabled: key === PageKey.INDUSTRY,
},
);
};
useNavigation({
interaction: messageOrInteraction,
key: 'main',
pages: [
mainPage(PageKey.MAIN, locale),
attributesPage(PageKey.ATTRIBUTES, locale),
fittingPage(PageKey.FITTING, locale),
skillsPage(PageKey.SKILLS, locale),
industryPage(PageKey.INDUSTRY, locale),
],
context: { type, buildButtonRow, interaction: messageOrInteraction },
updateContext,
saveResume,
});
}

View File

@@ -0,0 +1,189 @@
import { renderThreeColumns, type Page } from '@lib/discord';
import { EmbedBuilder } from 'discord.js';
import {
attributeOrdering,
type Type,
CommonAttribute,
eveRefLink,
getTypeIconUrl,
typeHasAnyAttribute,
getGroup,
typeGetAttribute,
getUnit,
renderUnit,
} from 'star-kitten-lib/eve';
import type { PageKey, TypeContext } from '../_old_ItemLookup';
export function attributesPage(key: PageKey.ATTRIBUTES, locale: string = 'en'): Page<TypeContext> {
return {
key,
content: async (context: TypeContext) => {
const type = context.type;
const embed = new EmbedBuilder()
.setTitle(type.name[locale] ?? type.name.en)
.setThumbnail(getTypeIconUrl(type))
.setURL(eveRefLink(type.type_id))
.setFooter({ text: `id: ${type.type_id}` })
.setColor('Green');
const embeds = [embed];
const fields = [];
if (type.dogma_attributes) {
const useOrders =
getGroup(type.group_id).category_id === 11
? attributeOrdering['11']
: getGroup(type.group_id).category_id === 87
? attributeOrdering['87']
: attributeOrdering.default;
Object.entries(useOrders).map((pair) => {
const [attributePath, attrs] = pair;
const combined = attrs['groupedAttributes']
? attrs.normalAttributes.concat(...(attrs['groupedAttributes']?.map(([name, id]) => id) ?? []))
: attrs.normalAttributes;
if (!typeHasAnyAttribute(type, combined)) return;
const split = attributePath.split('/');
const name = split[split.length - 1];
fields.push(
...renderThreeColumns(
name,
getAttributeNames(type, combined, locale),
[],
getAttributeValues(type, combined, locale),
),
);
});
}
// for (const [name, attrs] of Object.entries(attrMap)) {
// if (!type.hasAnyAttribute(attrs)) continue;
// if (name === 'Cargo | Drones' && type.group.category.category_id === CommonCategory.MODULE) continue;
// fields.push(...renderThreeColumns(
// name,
// getAttributeNames(type, attrs, locale),
// [],
// getAttributeValues(type, attrs, locale)
// ));
// }
// there is a max number of 24 fields per embed
embed.addFields(fields.splice(0, 24));
while (fields.length > 0) {
embeds.push(new EmbedBuilder().addFields(fields.splice(0, 24)));
}
return {
type: 'page',
embeds,
components: [context.buildButtonRow(key, context)],
};
},
};
}
const structureAttrs = [
CommonAttribute.StructureHitpoints,
CommonAttribute.Mass,
CommonAttribute.Volume,
CommonAttribute.InertiaModifier,
CommonAttribute.StructureEMResistance,
CommonAttribute.StructureThermalResistance,
CommonAttribute.StructureKineticResistance,
CommonAttribute.StructureExplosiveResistance,
];
const droneAttrs = [CommonAttribute.CargoCapacity, CommonAttribute.DroneBandwidth, CommonAttribute.DroneCapacity];
const armorAttrs = [
CommonAttribute.ArmorHitpoints,
CommonAttribute.ArmorEMResistance,
CommonAttribute.ArmorThermalResistance,
CommonAttribute.ArmorKineticResistance,
CommonAttribute.ArmorExplosiveResistance,
];
const shieldAttrs = [
CommonAttribute.ShieldCapacity,
CommonAttribute.ShieldRechargeTime,
CommonAttribute.ShieldEMResistance,
CommonAttribute.ShieldThermalResistance,
CommonAttribute.ShieldKineticResistance,
CommonAttribute.ShieldExplosiveResistance,
];
const elResAttrs = [
CommonAttribute.CapacitorWarfareResistance,
CommonAttribute.StasisWebifierResistance,
CommonAttribute.WeaponDisruptionResistance,
];
const capAttrs = [CommonAttribute.CapacitorCapacity, CommonAttribute.CapacitorRechargeTime];
const targetAttrs = [
CommonAttribute.MaxTargetRange,
CommonAttribute.MaxLockedTargets,
CommonAttribute.SignatureRadius,
CommonAttribute.ScanResolution,
CommonAttribute.RadarSensorStrength,
CommonAttribute.MagnetometricSensorStrength,
CommonAttribute.GravimetricSensorStrength,
CommonAttribute.LadarSensorStrength,
];
const jumpAttrs = [
CommonAttribute.JumpDriveCapacitorNeed,
CommonAttribute.MaxJumpRange,
CommonAttribute.JumpDriveFuelNeed,
CommonAttribute.JumpDriveConsumptionAmount,
CommonAttribute.FuelBayCapacity,
CommonAttribute.ConduitJumpConsumptionAmount,
CommonAttribute.COnduitJumpPassengerCapacity,
];
const propAttrs = [CommonAttribute.MaxVelocity, CommonAttribute.WarpSpeed];
const weaponAttrs = [
CommonAttribute.DamageMultiplier,
CommonAttribute.AccuracyFalloff,
CommonAttribute.OptimalRange,
CommonAttribute.RateOfFire,
CommonAttribute.TrackingSpeed,
CommonAttribute.ReloadTime,
CommonAttribute.ActivationTime,
CommonAttribute.ChargeSize,
CommonAttribute.UsedWithCharge1,
CommonAttribute.UsedWithCharge2,
];
const eWarAttrs = [CommonAttribute.MaxVelocityBonus];
const attrMap = {
Weapon: weaponAttrs,
Structure: structureAttrs,
Armor: armorAttrs,
Shield: shieldAttrs,
'Cargo | Drones': droneAttrs,
'Electronic Resistances': elResAttrs,
Capacitor: capAttrs,
Targeting: targetAttrs,
'Jump Drive Systems': jumpAttrs,
Propulsion: propAttrs,
'Electronic Warfare': eWarAttrs,
};
export function getAttributeNames(type: Type, ids: number[], locale: string = 'en') {
return ids
.map((id) => typeGetAttribute(type, id))
.filter((attr) => !!attr)
.map((attr) => `> ${attr.attribute.display_name[locale] ?? attr.attribute.display_name.en}`);
}
export function getAttributeValues(type: Type, ids: number[], locale: string = 'en') {
return ids
.map((id) => typeGetAttribute(type, id))
.filter((attr) => !!attr)
.map(
(attr) => `**${attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : attr.value}**`,
);
}

View File

@@ -0,0 +1,86 @@
import { EmbedBuilder } from 'discord.js';
import { renderThreeColumns, type Page } from '@lib/discord';
import { getAttributeNames, getAttributeValues } from './_old_attributes';
import { PageKey, type TypeContext } from '../_old_ItemLookup';
import {
eveRefLink,
getTypeIconUrl,
getTypeVariants,
typeHasAnyAttribute,
CommonAttribute,
renderTypeEveRefLink,
} from 'star-kitten-lib/eve';
export function fittingPage(key: string = PageKey.FITTING, locale: string = 'en'): Page<TypeContext> {
return {
key,
content: async (context: TypeContext) => {
const type = context.type;
const embed = new EmbedBuilder()
.setTitle(type.name[locale] ?? type.name.en)
.setThumbnail(getTypeIconUrl(type))
.setURL(eveRefLink(type.type_id))
.setFooter({ text: `id: ${type.type_id}` })
.setColor('Green');
const fields = [];
for (const [name, attrs] of Object.entries(attrMap)) {
if (!typeHasAnyAttribute(type, attrs)) continue;
fields.push(
...renderThreeColumns(
name,
getAttributeNames(type, attrs, locale),
[],
getAttributeValues(type, attrs, locale),
),
);
}
// get variants
{
if (getTypeVariants(type).length > 0) {
getTypeVariants(type).map((v) => {
fields.push({
name: `${v.metaGroup.name[locale] ?? v.metaGroup.name.en} variants`,
value: v.types.map((t) => renderTypeEveRefLink(t, locale)).join('\n'),
});
});
}
}
if (fields.length === 0) {
return {
type: 'page',
embeds: [embed.setDescription('This item does not have any fitting attributes.')],
components: [context.buildButtonRow(key, context)],
};
}
embed.addFields(fields);
return {
type: 'page',
embeds: [embed],
components: [context.buildButtonRow(key, context)],
};
},
};
}
const shipOutputAttrs = [CommonAttribute.PowergridOutput, CommonAttribute.CPUOutput];
const hardpointAttrs = [CommonAttribute.TurretHardpoints, CommonAttribute.LauncherHardpoints];
const moduleAttrs = [CommonAttribute.HighSlots, CommonAttribute.MediumSlots, CommonAttribute.LowSlots];
const rigAttrs = [CommonAttribute.RigSlots, CommonAttribute.RigSize, CommonAttribute.Calibration];
const moduleFittingAttrs = [CommonAttribute.CPUUsage, CommonAttribute.PowergridUsage, CommonAttribute.ActivationCost];
const attrMap = {
'Ship Output': shipOutputAttrs,
Hardpoints: hardpointAttrs,
Modules: moduleAttrs,
Rigs: rigAttrs,
Fitting: moduleFittingAttrs,
};

View File

@@ -0,0 +1,115 @@
import { renderThreeColumns, type Page } from '@lib/discord';
import { EmbedBuilder } from 'discord.js';
import {
getBlueprint,
type ManufacturingActivity,
eveRefLink,
getType,
getSchematic,
getTypeIconUrl,
getTypeBlueprints,
getTypeSchematics,
} from 'star-kitten-lib/eve';
import type { PageKey, TypeContext } from '../_old_ItemLookup';
export function industryPage(key: PageKey.INDUSTRY, locale: string = 'en'): Page<TypeContext> {
return {
key,
content: async (context: TypeContext) => {
const type = context.type;
const embed = new EmbedBuilder()
.setTitle(type.name[locale] ?? type.name.en)
.setThumbnail(getTypeIconUrl(type))
.setURL(eveRefLink(type.type_id))
.setFooter({ text: `id: ${type.type_id}` })
.setColor('Green');
let description = '';
const fields = [];
const bps = getTypeBlueprints(type);
if (bps.length > 0) {
bps.map((bp) => {
const type = bp.blueprint;
const blueprint = getBlueprint(bp.blueprint.type_id);
const activity = blueprint.activities[bp.activity];
description += `### Blueprint\n`;
description += `[${type.name[locale] ?? type.name.en}](${eveRefLink(type.type_id)})\n`;
// fields.push({
// name: 'Blueprints',
// value: bps.map(bp => {
// const type = bp.blueprint;
// return `[${type.name[locale] ?? type.name.en}](${type.eveRefLink})`;
// })
// });
if (activity['materials']) {
const manufacturing = activity as ManufacturingActivity;
if (manufacturing.materials) {
description += '### Materials\n```';
description += Object.values(manufacturing.materials)
.map((m) => {
const t = getType(m.type_id);
return `${t.name[locale] ?? t.name.en} ${m.quantity}`;
})
.join('\n');
description += '```';
// fields.push(...renderThreeColumns(
// 'Materials',
// Object.values(manufacturing.materials).map(m => {
// const t = getType(m.type_id);
// return `[${t.name[locale] ?? t.name.en}](${t.eveRefLink})`;
// }),
// [],
// Object.values(manufacturing.materials).map(m => {
// const t = getType(m.type_id);
// return `x**${m.quantity}**`;
// }),
// ));
}
}
});
}
const schematics = getTypeSchematics(type);
if (schematics.length > 0) {
schematics.map((type) => {
const schematic = getSchematic(type.type_id);
fields.push({
name: 'Schematic',
value: `[${type.name[locale] ?? type.name.en}](${eveRefLink(type.type_id)})`,
});
fields.push(
...renderThreeColumns(
'Materials',
Object.values(schematic.materials).map((m) => {
const t = getType(m.type_id);
return `[${t.name[locale] ?? t.name.en}](${eveRefLink(t.type_id)})`;
}),
[],
Object.values(schematic.materials).map((m) => {
return `x**${m.quantity}**`;
}),
),
);
});
}
if (description === '') {
description = 'No blueprints or schematics found';
}
embed.addFields(fields);
embed.setDescription(description);
return {
type: 'page',
embeds: [embed],
components: [context.buildButtonRow(key, context)],
};
},
};
}

View File

@@ -0,0 +1,134 @@
import { coloredText, renderThreeColumns, WHITE_SPACE, type Page } from '@lib/discord';
import { EmbedBuilder } from 'discord.js';
import type { Type } from 'star-kitten-lib/eve';
import { eveRefLink, getCharacterSkills, getGroup, getTypeIconUrl, getTypeSkills } from 'star-kitten-lib/eve';
import { CommonCategory } from 'star-kitten-lib/eve';
import type { PageKey, TypeContext } from '../_old_ItemLookup';
import { CharacterHelper, UserHelper } from 'star-kitten-lib/db';
function canUseText(type: Type) {
const category = getGroup(type.group_id).category_id;
switch (category) {
case CommonCategory.SHIP:
return 'fly this ship';
case CommonCategory.DRONE:
return 'use this drone';
case CommonCategory.MODULE:
return 'use this module';
default:
return 'use this item';
}
}
export function skillsPage(key: PageKey.SKILLS, locale: string = 'en'): Page<TypeContext> {
return {
key: 'skills',
content: async (context: TypeContext) => {
const type = context.type;
if (!type.required_skills || type.required_skills.length === 0) {
return {
type: 'page',
embeds: [
new EmbedBuilder()
.setTitle(type.name[locale] ?? type.name.en)
.setDescription('This item does not require any skills to use.')
.setThumbnail(getTypeIconUrl(type))
.setURL(eveRefLink(type.type_id))
.setFooter({ text: `id: ${type.type_id}` })
.setColor('Green'),
],
components: [context.buildButtonRow(key, context)],
};
}
const user = UserHelper.findByDiscordId(context.interaction.user.id);
const main = CharacterHelper.find(user.mainCharacterID);
const skills = main && (await getCharacterSkills(main));
const characterSkills: { [key: number]: number } =
skills && skills?.skills.reduce((acc, skill) => ({ ...acc, [skill.skill_id]: skill.trained_skill_level }), {});
const embed = new EmbedBuilder()
.setTitle(type.name[locale] ?? type.name.en)
.setThumbnail(getTypeIconUrl(type))
.setURL(eveRefLink(type.type_id))
.setFooter({ text: `id: ${type.type_id} -- ◼ = trained | ☒ = required but not trained` });
let description = '';
description += '### Required Skills\n```\n';
description += getTypeSkills(type)
.map((skillLevel) => `${skillLevel.skill.name[locale] ?? skillLevel.skill.name.en} ${skillLevel.level}`)
.join('\n');
description += '```';
let canFly = true;
if (characterSkills) {
if (getTypeSkills(type).every((skillLevel) => characterSkills[skillLevel.skill.type_id] >= skillLevel.level)) {
description += coloredText(`${main.name} can ${canUseText(type)}`, 'green');
canFly = true;
} else {
description += coloredText(`${main.name} cannot ${canUseText(type)}`, 'red');
canFly = false;
}
}
embed.setDescription(description);
embed.addFields(
renderThreeColumns('', getSkillNames(type, locale), [], getSkillLevels(type, characterSkills).map(renderLevel)),
);
embed.setColor(canFly ? 'Green' : 'Red');
return {
type: 'page',
embeds: [embed],
components: [context.buildButtonRow(key, context)],
};
},
};
}
function getSkillNames(type: Type, locale: string, depth: number = 0) {
let spacing = '';
for (let i = 0; i < depth; ++i) {
spacing += WHITE_SPACE;
}
let names: string[] = [];
getTypeSkills(type).forEach((skillLevel) => {
names.push(
`${spacing}[${skillLevel.skill.name[locale] ?? skillLevel.skill.name.en}](${skillLevel.skill.eveRefLink})`,
);
if (skillLevel.skill.skills.length > 0) {
names.push(...getSkillNames(skillLevel.skill, locale, depth + 1));
}
});
return names;
}
interface RequiredLevel {
required: number;
have: number;
}
// skills is a map of skill_id to trained_skill_level
function getSkillLevels(type: Type, skills?: { [key: number]: number }): RequiredLevel[] {
let levels: RequiredLevel[] = [];
getTypeSkills(type).forEach((skillLevel) => {
levels.push({
required: skillLevel.level,
have: skills ? skills[skillLevel.skill.type_id] || 0 : 0,
});
if (skillLevel.skill.skills.length > 0) {
levels.push(...getSkillLevels(skillLevel.skill, skills));
}
});
return levels;
}
function renderLevel(level: RequiredLevel) {
let str = '';
for (let i = 1; i <= 5; ++i) {
str += i <= level.required ? (level.have >= i ? '◼' : '☒') : level.have >= i ? '◼' : '▢';
// shapes to test with:
// '■' '▰' '▱' '▨' '▧' '◼' '▦' '▩' '▥' '▤' '▣' '▢' '◪' '◫' '◩' '◨' '◧'
}
return str + `${WHITE_SPACE}${level.have}/${level.required}`;
}

View File

@@ -0,0 +1,140 @@
import { renderSubroutes, type Page } from '@star-kitten/discord/pages';
import type { SearchState } from '../search.command';
import {
ButtonStyle,
container,
section,
separator,
text,
thumbnail,
Padding,
} from '@star-kitten/discord/components';
import {
getGroup,
getType,
getUnit,
renderUnit,
typeGetAttribute,
typeHasAnyAttribute,
type Type,
} from '@star-kitten/eve/models';
import { attributeOrdering } from '@star-kitten/eve';
import { searchActionRow } from './helpers';
import { toTitleCase } from '@star-kitten/util/text.js';
enum Images {
ATTRIBUTES = 'https://iili.io/KTbaMR2.md.webp',
DEFENSES = 'https://iili.io/KTbSVoX.md.webp',
FITTING = 'https://iili.io/KufiFYG.md.webp',
FACILITIES = 'https://iili.io/KufikGt.md.webp',
}
const attributeCategoryMap = {
structure: 'UI/Fitting/Structure',
armor: 'UI/Common/Armor',
shield: 'UI/Common/Shield',
ewar: 'UI/Common/EWarResistances',
capacitor: 'UI/Fitting/FittingWindow/Capacitor',
targeting: 'UI/Fitting/FittingWindow/Targeting',
facilities: 'UI/InfoWindow/SharedFacilities',
fighters: 'UI/InfoWindow/FighterFacilities',
on_death: 'UI/InfoWindow/OnDeath',
jump_drive: 'UI/InfoWindow/JumpDriveSystems',
propulsion: 'UI/Compare/Propulsion',
};
const groupedCategories = [
// defenses
['shield', 'armor', 'structure', 'ewar'],
// fittings
['capacitor', 'targeting', 'propulsion'],
// facilities
['facilities', 'fighters', 'on_death', 'jump_drive'],
];
function getAttributeOrdering(type: Type) {
const group = getGroup(type.group_id);
switch (group.category_id) {
case 11:
return attributeOrdering['11'];
case 87:
return attributeOrdering['87'];
default:
return attributeOrdering.default;
}
}
const bannerMap = {
shield: Images.DEFENSES,
armor: Images.DEFENSES,
structure: Images.DEFENSES,
ewar: Images.DEFENSES,
capacitor: Images.FITTING,
targeting: Images.FITTING,
propulsion: Images.FITTING,
facilities: Images.FACILITIES,
fighters: Images.FACILITIES,
on_death: Images.FACILITIES,
jump_drive: Images.FACILITIES,
};
const page: Page<SearchState> = {
key: 'attributes',
render: (context) => {
const type = getType(context.state.data.type_id);
const ordering = getAttributeOrdering(type);
return {
components: [
container(
{},
section(
thumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
text(`# [${type.name.en}](https://everef.net/types/${type.type_id})\n## Attributes`),
),
...renderSubroutes(
context,
'attributes',
groupedCategories.map((group) =>
group.map((cat) => {
const attrCat = ordering[attributeCategoryMap[cat]];
const attrs = attrCat.groupedCategories
? attrCat.groupedCategories.map(([name, id]) => id).concat(attrCat.normalAttributes) || []
: attrCat.normalAttributes;
if (!typeHasAnyAttribute(type, attrs)) {
return undefined;
}
return {
label: toTitleCase(cat.replace('_', ' ')),
value: cat,
banner: bannerMap[cat],
};
}),
),
(currentRoute) => {
const lines: string[] = [];
const attrCat = ordering[attributeCategoryMap[currentRoute]];
const attrs = attrCat.groupedCategories
? attrCat.groupedCategories.map(([name, id]) => id).concat(attrCat.normalAttributes) || []
: attrCat.normalAttributes;
attrs.map((attrId) => {
const attr = typeGetAttribute(type, attrId);
if (!attr) return;
const unit = attr.attribute.unit_id ? renderUnit(getUnit(attr.attribute.unit_id), attr.value) : '';
lines.push(`${attr.attribute.display_name.en.padEnd(24)} ${unit}`);
});
return text('```\n' + lines.join('\n') + '\n```');
},
{ style: ButtonStyle.SECONDARY },
),
separator(Padding.LARGE),
searchActionRow('attributes'),
),
],
};
},
};
export default page;

View File

@@ -0,0 +1,11 @@
import { actionRow, button } from '@star-kitten/discord/components';
export function searchActionRow(pageKey: string) {
return actionRow(
button('Main', 'main', { disabled: pageKey === 'main' }),
button('Attributes', 'attributes', { disabled: pageKey === 'attributes' }),
button('Fittings', 'fittings', { disabled: pageKey === 'fittings' }),
button('Skills', 'skills', { disabled: pageKey === 'skills' }),
button('Industry', 'industry', { disabled: pageKey === 'industry' }),
);
}

View File

@@ -0,0 +1,3 @@
export * from './_old_industry';
export * from './main';
export * from './attributes';

View File

@@ -0,0 +1,80 @@
import type { Page } from '@star-kitten/discord/pages';
import type { SearchState } from '../search.command';
import { container, gallery, section, text, thumbnail, urlButton } from '@star-kitten/discord/components';
import { formatNumberToShortForm } from '@star-kitten/util/text.js';
import { searchActionRow } from './helpers';
import { cleanText, typeSearch } from '@star-kitten/eve';
import { getRoleBonuses, getSkillBonuses, getType } from '@star-kitten/eve/models';
import { fetchPrice } from '@star-kitten/eve/third-party/janice.js';
const page: Page<SearchState> = {
key: 'main',
render: async (context) => {
if (!context.state.data.type_id && context.interaction.isApplicationCommand()) {
const typeName = context.interaction.data.options?.find((opt) => opt.name === 'name')?.value;
const found = await typeSearch(typeName as string);
if (!found) {
return {
components: [text(`No item found for: ${typeName}`)],
};
}
context.state.data.type_id = found.type_id;
}
const type = getType(context.state.data.type_id);
const skillBonuses = getSkillBonuses(type);
const roleBonuses = getRoleBonuses(type);
const price = await fetchPrice(type.type_id);
return {
components: [
container(
{},
section(
thumbnail(`https://images.evetech.net/types/${type.type_id}/icon`),
text(`
# [${type.name.en}](https://everef.net/types/${type.type_id})
${skillBonuses
.map((bonus) => {
return `## Bonus per level of ${bonus.skill.name.en}
${bonus.bonuses
.sort((a, b) => a.importance - b.importance)
.map((b) => `${b.bonus}${b.unit?.display_name ?? '-'} ${cleanText(b.bonus_text.en)}`)
.join('\n')}`;
})
.join('\n')}
${
roleBonuses.length > 0
? `\n## Role Bonuses
${roleBonuses
.sort((a, b) => a.importance - b.importance)
.map((b) => `${b.bonus ?? ''}${b.unit?.display_name ?? '-'} ${cleanText(b.bonus_text.en)}`)
.join('\n')}`
: ''
}
`),
),
gallery({
url: 'https://iili.io/KTPCFRt.md.webp',
}),
// createSeparator(Padding.LARGE),
section(
urlButton('View on EVE Tycoon', `https://evetycoon.com/market/${type.type_id}`),
text(
`## Buy: ${price ? formatNumberToShortForm(price.buyAvgFivePercent) : '--'} ISK
## Sell: ${price ? formatNumberToShortForm(price.sellAvgFivePercent) : '--'} ISK`,
),
),
text(`-# Type Id: ${type.type_id}`),
searchActionRow('main'),
),
],
};
},
};
export default page;

View File

@@ -0,0 +1,68 @@
import {
createChatCommand,
stringOption,
type CommandContext,
type ExecutableInteraction,
} from '@star-kitten/discord';
import { usePages } from '@star-kitten/discord/pages';
import { initializeTypeSearch, typeSearchAutoComplete } from '@star-kitten/eve';
import main from './pages/main';
import attributes from './pages/attributes';
let now = Date.now();
console.debug('Initializing type search...');
await initializeTypeSearch().catch((e) => {
console.error('Failed to initialize type search', e);
process.exit(1);
});
console.debug(`Type search initialized. Took ${Date.now() - now}ms`);
export interface SearchState {
type_id: number;
}
export default createChatCommand(
{
name: 'search',
description: 'Search for a type',
options: [
stringOption({
name: 'name',
description: 'The type name to search for',
autocomplete: true,
required: true,
}),
],
},
execute,
);
async function execute(interaction: ExecutableInteraction, ctx: CommandContext) {
if (interaction.isAutocomplete()) {
const focusedOption = interaction.data.options?.find((opt) => opt.focused);
if (focusedOption?.name === 'name') {
const value = focusedOption.value as string;
const results = await typeSearchAutoComplete(value);
if (results) {
await interaction.result(results);
} else {
await interaction.result([]);
}
}
return;
}
usePages<SearchState>(
{
pages: {
main,
attributes,
},
initialPage: 'main',
ephemeral: false,
},
interaction,
ctx,
);
}

4
src/main.ts Normal file
View File

@@ -0,0 +1,4 @@
import { startBot } from '@star-kitten/discord';
import './commands';
startBot();

36
tsconfig.json Normal file
View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"typeRoots": ["src/types", "node_modules/@types"],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": false,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"paths": {
"@/*": ["./src/*"],
},
"composite": true,
},
"include": ["src"],
"exclude": ["node_modules", "dist", "build", "**/*.test.ts"]
}