This commit is contained in:
artie 2025-02-08 16:11:43 +01:00
commit c0da4083da
27 changed files with 1126 additions and 0 deletions

177
.gitignore vendored Normal file
View File

@ -0,0 +1,177 @@
# 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
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# 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
data/temp/*

15
README.md Normal file
View File

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

198
bun.lock Normal file
View File

@ -0,0 +1,198 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "artemis",
"dependencies": {
"@discordjs/core": "^2.0.1",
"@sapphire/discord.js-utilities": "^7.3.2",
"cheerio": "^1.0.0",
"discord.js": "^14.17.3",
"ky": "^1.7.4",
"winston": "^3.17.0",
"zod": "^3.24.1",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5.0.0",
},
},
},
"packages": {
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
"@discordjs/builders": ["@discordjs/builders@1.10.0", "", { "dependencies": { "@discordjs/formatters": "^0.6.0", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.37.114", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg=="],
"@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
"@discordjs/core": ["@discordjs/core@2.0.1", "", { "dependencies": { "@discordjs/rest": "^2.4.1", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^2.0.1", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.37.114" } }, "sha512-tj//rnPhdniq+Clxq0tq5wI7QTMH4IgyXkUFI32+ashIRpFJ1+J1btNG3FA74oATFu2sYy/zT9CKLWW4jp2RGQ=="],
"@discordjs/formatters": ["@discordjs/formatters@0.6.0", "", { "dependencies": { "discord-api-types": "^0.37.114" } }, "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw=="],
"@discordjs/rest": ["@discordjs/rest@2.4.2", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.37.114", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.19.8" } }, "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g=="],
"@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],
"@discordjs/ws": ["@discordjs/ws@2.0.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/rest": "^2.4.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@types/ws": "^8.5.12", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.37.114", "tslib": "^2.6.3", "ws": "^8.18.0" } }, "sha512-5etVbXdwThIT5+KfU+uiBh358Ql58f2vf1W9B5ZXxaZuIlpiX/VC/G0Lr/xuEcs6cL+/HZtEIUf0p9dQfEDang=="],
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
"@sapphire/discord-utilities": ["@sapphire/discord-utilities@3.4.4", "", { "dependencies": { "discord-api-types": "^0.37.114" } }, "sha512-P7bCL5U2s+U5oy9OFpC4lr4Y0bGoNrSCPu2bgxP9AaqK90hcwpoLWWKouvGgaY9tCbUZmaf8E98/SlZZoakhAQ=="],
"@sapphire/discord.js-utilities": ["@sapphire/discord.js-utilities@7.3.2", "", { "dependencies": { "@sapphire/discord-utilities": "^3.4.4", "@sapphire/duration": "^1.1.4", "@sapphire/utilities": "^3.18.1", "tslib": "^2.8.1" } }, "sha512-eEs6SjZghc7SlSRfirXzfJNy7sT9VxBtX32Zz33u8sxVq2X2/W6++klmDuIi6LSLrcsAlo9xWxcR+lL0pdaIEQ=="],
"@sapphire/duration": ["@sapphire/duration@1.1.4", "", {}, "sha512-hxtuE8HvmWcRok2A10lJ+ic8qY0oYGTTn44XmESUYJYYSVJWmqlCH1LnNYi6Ul+LRjxNUfFQEL/TJS0GJ+8kew=="],
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
"@sapphire/utilities": ["@sapphire/utilities@3.18.2", "", {}, "sha512-QGLdC9+pT74Zd7aaObqn0EUfq40c4dyTL65pFnkM6WO1QYN7Yg/s4CdH+CXmx0Zcu6wcfCWILSftXPMosJHP5A=="],
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
"@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
"cheerio": ["cheerio@1.0.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "encoding-sniffer": "^0.2.0", "htmlparser2": "^9.1.0", "parse5": "^7.1.2", "parse5-htmlparser2-tree-adapter": "^7.0.0", "parse5-parser-stream": "^7.1.2", "undici": "^6.19.5", "whatwg-mimetype": "^4.0.0" } }, "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww=="],
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
"css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="],
"css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="],
"discord-api-types": ["discord-api-types@0.37.119", "", {}, "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg=="],
"discord.js": ["discord.js@14.17.3", "", { "dependencies": { "@discordjs/builders": "^1.10.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.0", "@discordjs/rest": "^2.4.2", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.0", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.37.114", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", "undici": "6.19.8" } }, "sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
"encoding-sniffer": ["encoding-sniffer@0.2.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"ky": ["ky@1.7.4", "", {}, "sha512-zYEr/gh7uLW2l4su11bmQ2M9xLgQLjyvx58UyNM/6nuqyWFHPX5ktMjvpev3F8QWdjSsHUpnWew4PBCswBNuMQ=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"magic-bytes.js": ["magic-bytes.js@1.10.0", "", {}, "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
"parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="],
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
"parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"undici": ["undici@6.19.8", "", {}, "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"discord.js/@discordjs/ws": ["@discordjs/ws@1.2.0", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.4.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.37.114", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg=="],
"discord.js/@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
}
}

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "artemis",
"module": "index.ts",
"type": "module",
"main": "src/index.ts",
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun run src/index.ts",
"sync": "bun run src/scripts/sync.ts",
"test": "bun run src/scripts/test.ts"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@discordjs/core": "^2.0.1",
"@sapphire/discord.js-utilities": "^7.3.2",
"cheerio": "^1.0.0",
"discord.js": "^14.17.3",
"ky": "^1.7.4",
"winston": "^3.17.0",
"zod": "^3.24.1"
}
}

6
src/api.ts Normal file
View File

@ -0,0 +1,6 @@
import { env } from "./env";
import { API } from "@discordjs/core";
import { REST } from "discord.js";
const rest = new REST().setToken(env.DISCORD_TOKEN);
export const api = new API(rest);

130
src/client.ts Normal file
View File

@ -0,0 +1,130 @@
import { Client, Collection, GatewayIntentBits } from "discord.js";
import { env } from "./env";
import { ActivityType, API, InteractionContextType } from "@discordjs/core";
import { api } from "./api";
import type { Command } from "./types/command";
import path from "node:path";
import fs from "node:fs/promises";
import { log } from "./utils/logger";
import { pluralize } from "./utils/functions";
import { DEV } from "./utils/constants";
export class ArtemisClient extends Client {
public api: API;
public ownerId = env.OWNER_ID;
public commands = new Collection<string, Command>();
constructor() {
super({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.DirectMessages,
],
allowedMentions: {
parse: [],
},
presence: {
activities: [{ name: "🩷", type: ActivityType.Custom }],
},
});
this.api = api;
}
async setup() {
Promise.all([this.loadCommands(), this.registerEvents()]);
}
async loadCommands() {
const commandsDir = path.join(import.meta.dir, "commands");
const categories = await fs
.readdir(commandsDir)
.then((categories) =>
categories.filter((category) => !category.includes("."))
);
const promises: Promise<void>[] = [];
for (const category of categories) {
const files = await fs
.readdir(path.join(commandsDir, category))
.then((files) => files.filter((file) => file.endsWith(".ts")));
for (const file of files) {
promises.push(
import(path.join(commandsDir, category, file)).then(
({ default: command }: { default: Command }) => {
this.commands.set(command.data.name, {
...command,
category,
});
}
)
);
}
}
await Promise.all(promises);
log.info(`Loaded ${pluralize(this.commands.size, "application command")}`);
}
async registerEvents() {
const eventsDir = path.join(import.meta.dir, "events");
const files = await fs
.readdir(eventsDir)
.then((files) => files.filter((file) => file !== "index.ts"));
for (const file of files) {
const { default: event } = await import(path.join(eventsDir, file));
this[event.once ? "once" : "on"](event.name, event.execute);
}
log.info(`Registered ${pluralize(files.length, "event")}`);
}
async syncCommands() {
if (!this.commands.size) {
log.warn("No commands were loaded, skipping registration");
return;
}
const publicCommands = this.commands
.filter((command) => !command.isOwnerOnly)
.map((command) => command.data.toJSON());
const ownerCommands = this.commands
.filter((command) => command.isOwnerOnly)
.map((command) => command.data.toJSON());
let guildCount = 0;
let globalCount = 0;
if (DEV) {
await this.api.applicationCommands
.bulkOverwriteGuildCommands(env.APPLICATION_ID, env.DEV_GUILD_ID, [
...publicCommands,
...ownerCommands,
])
.then((res) => (guildCount += res.length));
} else {
await this.api.applicationCommands
.bulkOverwriteGuildCommands(
env.APPLICATION_ID,
env.DEV_GUILD_ID,
ownerCommands
)
.then((res) => (guildCount += res.length));
await this.api.applicationCommands
.bulkOverwriteGlobalCommands(env.APPLICATION_ID, publicCommands)
.then((res) => (globalCount += res.length));
}
log.info(
`Successfully synced ${guildCount} guild and ${globalCount} global application commands`
);
return { guildCount, globalCount };
}
}
export const client = new ArtemisClient();

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

@ -0,0 +1,5 @@
import type { Command } from "../types/command";
export function defineCommand(command: Command) {
return command;
}

View File

@ -0,0 +1,131 @@
import {
bold,
EmbedBuilder,
inlineCode,
SlashCommandBuilder,
} from "discord.js";
import { defineCommand } from "..";
import { getDefinitions } from "../../utils/wiktionary";
import { stripHtml } from "../../utils/functions";
export default defineCommand({
data: new SlashCommandBuilder()
.setName("wiktionary")
.setDescription("Looks up a term on Wiktionary")
.addStringOption((option) =>
option
.setName("term")
.setDescription("The term to look up")
.setRequired(true)
.setAutocomplete(true)
),
async autocomplete(interaction) {
let language: string | undefined;
let term = interaction.options.getFocused().trim();
if (term.length < 3) {
await interaction.respond([]);
return;
}
const parsed = term.split(":");
if (parsed.length === 2) {
term = parsed[0].trim();
language = parsed[1].trim();
if (!language) {
await interaction.respond([]);
return;
}
}
const definitions = await getDefinitions(term);
if (!definitions) {
await interaction.respond([]);
return;
}
if (language) {
const choices = definitions
.filter((definition) =>
definition.language.toLowerCase().startsWith(language.toLowerCase())
)
.map((definition) => ({
name: `${term} (${definition.language})`,
value: `:${term}:${definition.languageCode}:`,
}))
.slice(0, 25);
await interaction.respond(choices);
return;
}
const choices = definitions
.map((definition) => ({
name: `${term} (${definition.language})`,
value: `:${term}:${definition.languageCode}:`,
}))
.slice(0, 25);
await interaction.respond(choices);
},
async execute(interaction) {
let term = interaction.options.getString("term", true);
let languageCode: string | undefined;
const parsed = term.match(/^:(?<term>.+):(?<languageCode>.+):$/);
if (parsed?.groups) {
term = parsed.groups.term;
languageCode = parsed.groups.languageCode;
}
const definitions = await getDefinitions(term);
if (!definitions) {
await interaction.reply({
content: "No definitions found",
});
return;
}
const definition = languageCode
? definitions.find((def) => def.languageCode === languageCode)
: definitions[0];
if (!definition) {
await interaction.reply({
content: "No definitions found",
});
return;
}
let description = definition.entries
.map((entry) => {
const name = entry.partOfSpeech;
const definitions = entry.definitions
.filter((def) => def.definition)
.map((def, i) => {
const prefix = inlineCode(`${i + 1}.`);
const definition = stripHtml(def.definition);
return `${prefix} ${definition.trim()}`;
})
.join("\n");
return `${bold(name)}\n${definitions}`;
})
.join("\n\n");
const embed = new EmbedBuilder()
.setAuthor({
name: `Wiktionary - ${definition.language}`,
iconURL:
"https://en.wiktionary.org/static/apple-touch/wiktionary/en.png",
url: `https://${
definition.languageCode
}.wiktionary.org/wiki/${encodeURIComponent(term)}`,
})
.setTitle(term)
.setColor(0xfefefe)
.setDescription(description);
await interaction.reply({ embeds: [embed] });
},
});

View File

@ -0,0 +1,19 @@
import { MessageFlags, SlashCommandBuilder } from "discord.js";
import { defineCommand } from "..";
import { restart } from "../../utils/restart";
export default defineCommand({
data: new SlashCommandBuilder()
.setName("restart")
.setDescription("Restarts the bot"),
isOwnerOnly: true,
async execute(interaction) {
await interaction.reply({
content: "Restarting...",
flags: MessageFlags.Ephemeral,
});
await restart(interaction.token);
},
});

View File

@ -0,0 +1,28 @@
import { MessageFlags, SlashCommandBuilder } from "discord.js";
import { client } from "../../client";
import { defineCommand } from "..";
export default defineCommand({
data: new SlashCommandBuilder()
.setName("sync")
.setDescription("Sync application commands"),
isOwnerOnly: true,
async execute(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const counts = await client.syncCommands();
if (!counts) {
await interaction.followUp({
content: "No commands to sync",
});
return;
}
const { guildCount, globalCount } = counts;
await interaction.followUp({
content: `Successfully synced ${guildCount} guild and ${globalCount} global application commands`,
});
},
});

View File

@ -0,0 +1,33 @@
import { inlineCode, SlashCommandBuilder } from "discord.js";
import { client } from "../../client";
import { defineCommand } from "..";
export default defineCommand({
data: new SlashCommandBuilder()
.setName("ping")
.setDescription("Useful latency data"),
async execute(interaction) {
if (client.ws.ping < 1) {
await interaction.reply(
":ping_pong: Pong!\nThe bot is still starting up, accurate latency will be available shortly."
);
return;
}
const msg = (
await interaction.reply({
content: `:ping_pong: Pong!\nWebSocket latency is ${inlineCode(
Math.round(client.ws.ping).toString()
)} ms.`,
withResponse: true,
})
).resource?.message!;
await msg.edit(
`${msg.content}\nAPI roundtrip latency is ${inlineCode(
(msg.createdTimestamp - interaction.createdTimestamp).toString()
)}ms.`
);
},
});

14
src/env.ts Normal file
View File

@ -0,0 +1,14 @@
import { z } from "zod";
const envSchema = z.object({
DISCORD_TOKEN: z.string(),
OWNER_ID: z.string(),
APPLICATION_ID: z.string(),
NODE_ENV: z
.enum(["development", "production"])
.optional()
.default("development"),
DEV_GUILD_ID: z.string(),
});
export const env = envSchema.parse(process.env);

20
src/events/clientReady.ts Normal file
View File

@ -0,0 +1,20 @@
import { env } from "../env";
import { Events } from "discord.js";
import { log } from "../utils/logger";
import { defineEvent } from ".";
import { maybeSendRestarted } from "../utils/restart";
export default defineEvent({
name: Events.ClientReady,
once: true,
async execute(client) {
log.info("Logged in", {
tag: client.user.tag,
id: client.user.id,
env: env.NODE_ENV,
});
await maybeSendRestarted();
},
});

6
src/events/index.ts Normal file
View File

@ -0,0 +1,6 @@
import type { ClientEvents } from "discord.js";
import type { Event } from "../types/event";
export function defineEvent<E extends keyof ClientEvents>(event: Event<E>) {
return event;
}

View File

@ -0,0 +1,81 @@
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
Events,
MessageFlags,
} from "discord.js";
import { client } from "../client";
import { log } from "../utils/logger";
import { defineEvent } from ".";
const running = new Map<string, number>();
const getRunning = (command: string) => running.get(command) ?? 0;
export default defineEvent({
name: Events.InteractionCreate,
async execute(interaction) {
if (interaction.isChatInputCommand()) {
await handleChatInputCommand(interaction);
} else if (interaction.isAutocomplete()) {
await handleAutocomplete(interaction);
}
},
});
async function handleChatInputCommand(
interaction: ChatInputCommandInteraction
) {
const command = client.commands.get(interaction.commandName);
if (!command) return;
if (command.isOwnerOnly && interaction.user.id !== client.ownerId) {
await interaction.reply({
content: "You do not have permission to use this command!",
flags: MessageFlags.Ephemeral,
});
return;
}
if (command.maxConcurrency) {
if (getRunning(command.data.name) >= command.maxConcurrency) {
await interaction.reply({
content: `This command can only be run ${command.maxConcurrency} times at a time.`,
flags: MessageFlags.Ephemeral,
});
return;
}
running.set(command.data.name, getRunning(command.data.name) + 1);
}
try {
await command.execute(interaction);
} catch (error) {
log.error(error);
await interaction[
interaction.replied || interaction.deferred ? "followUp" : "reply"
]({
content: "There was an error while executing this command!",
});
} finally {
if (command.maxConcurrency) {
running.set(
command.data.name,
Math.max(0, getRunning(command.data.name) - 1)
);
}
}
}
async function handleAutocomplete(interaction: AutocompleteInteraction) {
const command = client.commands.get(interaction.commandName);
if (!command) return;
if (command.autocomplete) {
try {
await command.autocomplete(interaction);
} catch (error) {
log.error(error);
}
}
}

5
src/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { env } from "./env";
import { client } from "./client";
await client.setup();
client.login(env.DISCORD_TOKEN);

9
src/scripts/sync.ts Normal file
View File

@ -0,0 +1,9 @@
import { client } from "../client";
import { log } from "../utils/logger";
async function main() {
await client.loadCommands();
await client.syncCommands();
}
main().catch(log.error);

7
src/scripts/test.ts Normal file
View File

@ -0,0 +1,7 @@
import { getDefinitions } from "../utils/wiktionary";
const definitions = await getDefinitions("appl");
if (!definitions) process.exit(1);
console.dir(definitions, { depth: null });

15
src/types/command.ts Normal file
View File

@ -0,0 +1,15 @@
import type {
AutocompleteInteraction,
ChatInputCommandInteraction,
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
} from "discord.js";
export interface Command {
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder;
execute(interaction: ChatInputCommandInteraction): Promise<void>;
autocomplete?(interaction: AutocompleteInteraction): Promise<void>;
category?: string;
maxConcurrency?: number;
isOwnerOnly?: boolean;
}

7
src/types/event.ts Normal file
View File

@ -0,0 +1,7 @@
import type { ClientEvents } from "discord.js";
export interface Event<Event extends keyof ClientEvents> {
name: Event;
once?: boolean;
execute(...args: ClientEvents[Event]): Promise<void>;
}

48
src/utils/components.ts Normal file
View File

@ -0,0 +1,48 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ChatInputCommandInteraction,
ComponentType,
} from "discord.js";
export async function confirmPrompt(
interaction: ChatInputCommandInteraction,
message: string
) {
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("confirm")
.setLabel("Confirm")
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId("cancel")
.setLabel("Cancel")
.setStyle(ButtonStyle.Danger)
);
const msg = (
await interaction.reply({
content: message,
components: [row],
withResponse: true,
})
).resource?.message!;
const confirmation = await msg
.awaitMessageComponent({
componentType: ComponentType.Button,
time: 60000,
filter: (i) => i.user.id === interaction.user.id,
dispose: true,
})
.catch(() => {
interaction.editReply({
content: "You took too long to respond.",
components: [],
});
});
msg.delete();
return confirmation?.customId === "confirm";
}

4
src/utils/constants.ts Normal file
View File

@ -0,0 +1,4 @@
import { env } from "../env";
export const DEV = env.NODE_ENV === "development";
export const PROD = env.NODE_ENV === "production";

41
src/utils/functions.ts Normal file
View File

@ -0,0 +1,41 @@
import * as cheerio from "cheerio";
export function noop() {}
export function pickRandom<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
export function sleep(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
}
export function chunk<T>(arr: T[], size: number): T[][];
export function chunk(arr: string, size: number): string[];
export function chunk(arr: any, size: number): any[] {
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
}
export function stripHtml(html: string) {
return cheerio.load(html).text();
}
export function pluralize(
value: number,
singular: string,
plural = singular + "s"
) {
return value === 1 ? `${value} ${singular}` : `${value} ${plural}`;
}
export function run<T>(fn: () => T): T {
return fn();
}
export async function silently<T extends Promise<any>>(p?: T) {
try {
return await p;
} catch {}
}

11
src/utils/logger.ts Normal file
View File

@ -0,0 +1,11 @@
import { createLogger, format, transports } from "winston";
import { DEV } from "./constants";
const { combine, simple, errors, prettyPrint } = format;
export const log = createLogger({
level: "info",
format: DEV
? combine(errors({ stack: true }), prettyPrint({ colorize: true }))
: combine(errors({ stack: true }), simple()),
transports: [new transports.Console()],
});

34
src/utils/restart.ts Normal file
View File

@ -0,0 +1,34 @@
import { Client, InteractionWebhook, MessageFlags } from "discord.js";
import { silently } from "./functions";
import { client } from "../client";
import { env } from "../env";
import { readFile, rm, writeFile } from "node:fs/promises";
import { log } from "./logger";
export async function restart(state: string) {
log.info("Shutting down...");
await writeFile("./data/temp/restart", state);
await client.destroy();
process.exit(0);
}
export async function maybeSendRestarted() {
const restartToken = await silently(readFile("./data/temp/restart", "utf-8"));
if (restartToken) {
const webhook = new InteractionWebhook(
client as Client<true>,
env.APPLICATION_ID,
restartToken
);
await webhook.send({
content: "Successfully restarted!",
flags: MessageFlags.Ephemeral,
});
await silently(rm("./data/temp/restart"));
}
}

37
src/utils/wiktionary.ts Normal file
View File

@ -0,0 +1,37 @@
import ky from "ky";
type ParsedExample = {
example: string;
};
type Definition = {
definition: string;
parsedExamples?: ParsedExample[];
};
type Entry = {
partOfSpeech: string;
language: string;
definitions: Definition[];
};
type DefinitionsResponse = {
[key: string]: Entry[];
};
const client = ky.create({
prefixUrl: "https://en.wiktionary.org/api/rest_v1",
throwHttpErrors: false,
});
export async function getDefinitions(word: string) {
const res = await client.get("page/definition/" + encodeURIComponent(word));
const data = await res.json<DefinitionsResponse>();
if (!res.ok || !data) return null;
return Object.entries(data).map(([languageCode, entries]) => ({
language: entries[0].language,
languageCode,
entries,
}));
}

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"allowJs": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true
}
}