Add initial bot
This commit is contained in:
parent
71e6196d40
commit
e1fffa26a5
|
@ -1,2 +1,3 @@
|
|||
node_modules/
|
||||
build/
|
||||
.env
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
<option name="myRunOnSave" value="true" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$schema": "https://swc.rs/schema.json",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"jsx": false,
|
||||
"dynamicImport": false,
|
||||
"privateMethod": false,
|
||||
"functionBind": false,
|
||||
"exportDefaultFrom": false,
|
||||
"exportNamespaceFrom": false,
|
||||
"decorators": false,
|
||||
"decoratorsBeforeExport": false,
|
||||
"topLevelAwait": true,
|
||||
"importMeta": false
|
||||
},
|
||||
"transform": null,
|
||||
"target": "es5",
|
||||
"loose": false,
|
||||
"externalHelpers": false,
|
||||
// Requires v1.2.50 or upper and requires target to be es2016 or upper.
|
||||
"keepClassNames": false
|
||||
},
|
||||
"minify": false
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
FROM node
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm ci
|
||||
|
||||
#TODO need to grab a bundler to build
|
||||
|
||||
ENTRYPOINT npm run dev
|
|
@ -9,13 +9,17 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.15.3"
|
||||
"cheerio": "^1.0.0",
|
||||
"discord.js": "^14.15.3",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc-node/register": "^1.10.9",
|
||||
"@swc/cli": "^0.4.0",
|
||||
"@swc/core": "^1.7.22",
|
||||
"vitest": "^2.0.5"
|
||||
"prettier": "^3.3.3",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest-fetch-mock": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
|
@ -1848,6 +1852,11 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
|
@ -1951,6 +1960,54 @@
|
|||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
|
||||
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"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"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/undici": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
|
||||
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
|
@ -1990,6 +2047,15 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
|
||||
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
@ -2004,6 +2070,32 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||
|
@ -2096,6 +2188,80 @@
|
|||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding-sniffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
|
||||
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.3",
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
|
@ -2105,6 +2271,17 @@
|
|||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
|
@ -2399,6 +2576,24 @@
|
|||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
|
@ -2427,6 +2622,17 @@
|
|||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
@ -2696,6 +2902,26 @@
|
|||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz",
|
||||
|
@ -2747,6 +2973,17 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
@ -2823,6 +3060,40 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
||||
"dependencies": {
|
||||
"entities": "^4.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz",
|
||||
"integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.2",
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-parser-stream": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||
"dependencies": {
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
|
@ -2933,6 +3204,21 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
|
@ -3117,6 +3403,11 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
|
@ -3389,6 +3680,12 @@
|
|||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/trim-repeated": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz",
|
||||
|
@ -3589,6 +3886,56 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest-fetch-mock": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.3.0.tgz",
|
||||
"integrity": "sha512-g6upWcL8/32fXL43/5f4VHcocuwQIi9Fj5othcK9gPO8XqSEGtnIZdenr2IaipDr61ReRFt+vaOEgo8jiUUX5w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-fetch": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
@ -10,12 +10,16 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.15.3"
|
||||
"cheerio": "^1.0.0",
|
||||
"discord.js": "^14.15.3",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc-node/register": "^1.10.9",
|
||||
"@swc/cli": "^0.4.0",
|
||||
"@swc/core": "^1.7.22",
|
||||
"vitest": "^2.0.5"
|
||||
"prettier": "^3.3.3",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest-fetch-mock": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
export type BookInfo = {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
source: string;
|
||||
url: string;
|
||||
// TODO isbn etc...
|
||||
};
|
||||
|
||||
export type BookInfoShort = {
|
||||
title: string;
|
||||
author: string;
|
||||
id: string;
|
||||
source: string;
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { BookInfo, BookInfoShort } from "./BookInfo";
|
||||
|
||||
export type BookSource = {
|
||||
search(title: string): Promise<BookInfoShort[]>;
|
||||
getBook(id: string): Promise<BookInfo>;
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import goodreads from "./index";
|
||||
import createFetchMock from "vitest-fetch-mock";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
fetchMocker.enableMocks();
|
||||
|
||||
// I don't want to hit the site every time the tests run
|
||||
const searchPage = fs.readFileSync(`${__dirname}/test/search.html`, "utf8");
|
||||
const bookPage = fs.readFileSync(`${__dirname}/test/book.html`, "utf8");
|
||||
|
||||
describe("goodreads", () => {
|
||||
beforeEach(async () => {
|
||||
fetchMocker.mockIf(
|
||||
"https://www.goodreads.com/search?query=democracy%20the%20god%20that%20failed",
|
||||
searchPage,
|
||||
);
|
||||
|
||||
fetchMocker.mockIf("https://www.goodreads.com/book/show/980031", bookPage);
|
||||
});
|
||||
|
||||
it("searches for a book", async () => {
|
||||
const bookResults = await goodreads.search("democracy the god that failed");
|
||||
expect(bookResults).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"author": "Hans-Hermann Hoppe",
|
||||
"id": "980031",
|
||||
"source": "goodreads",
|
||||
"title": "Democracy: The God That Failed",
|
||||
},
|
||||
{
|
||||
"author": "Krishnan Nayar",
|
||||
"id": "123061378",
|
||||
"source": "goodreads",
|
||||
"title": "Liberal Capitalist Democracy: The God That Failed",
|
||||
},
|
||||
{
|
||||
"author": "Hephaestus Books",
|
||||
"id": "12889008",
|
||||
"source": "goodreads",
|
||||
"title": "Articles on Books about Democracy, Including: Democracy in America, Democracy: The God That Failed, Deterring Democracy, the Coming Victory of Democracy, of Grunge and Government: Let's Fix This Broken Democracy",
|
||||
},
|
||||
{
|
||||
"author": "unknown author",
|
||||
"id": "207661975",
|
||||
"source": "goodreads",
|
||||
"title": "Democracy-The God That Failed: The Economics and Politics of Monarchy, Democracy, and Natural Order (Perspectives on Democratic Practice) by Hans-Hermann Hoppe(2001-07-30)",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it(`Gets a book's info`, async () => {
|
||||
const info = await goodreads.getBook("980031");
|
||||
expect(info).toMatchInlineSnapshot(`
|
||||
{
|
||||
"author": "Hans-Hermann Hoppe",
|
||||
"description": "The core of this book is a systematic treatment of the historic transformation of the West from monarchy to democracy. Revisionist in nature, it reaches the conclusion that monarchy is a lesser evil than democracy, but outlines deficiencies in both. Its methodology is axiomatic-deductive, allowing the writer to derive economic and sociological theorems, and then apply them to interpret historical events.A compelling chapter on time preference describes the progress of civilization as lowering time preferences as capital structure is built, and explains how the interaction between people can lower time all around, with interesting parallels to the Ricardian Law of Association. By focusing on this transformation, the author is able to interpret many historical phenomena, such as rising levels of crime, degeneration of standards of conduct and morality, and the growth of the mega-state. In underscoring the deficiencies of both monarchy and democracy, the author demonstrates how these systems are both inferior to a natural order based on private-property.Hoppe deconstructs the classical liberal belief in the possibility of limited government and calls for an alignment of conservatism and libertarianism as natural allies with common goals. He defends the proper role of the production of defense as undertaken by insurance companies on a free market, and describes the emergence of private law among competing insurers.Having established a natural order as superior on utilitarian grounds, the author goes on to assess the prospects for achieving a natural order. Informed by his analysis of the deficiencies of social democracy, and armed with the social theory of legitimation, he forsees secession as the likely future of the US and Europe, resulting in a multitude of region and city-states. This book complements the author's previous work defending the ethics of private property and natural order. Democracy - The God that Failed will be of interest to scholars and students of history, political economy, and political philosophy.",
|
||||
"id": "980031",
|
||||
"source": "goodreads",
|
||||
"title": "Democracy: The God That Failed",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
import { BookSource } from "../BookSource";
|
||||
import { BookInfo, BookInfoShort } from "../BookInfo";
|
||||
import * as cheerio from "cheerio";
|
||||
import { UserError } from "../../core/UserError";
|
||||
|
||||
const searchUrl = (query: string) =>
|
||||
`https://www.goodreads.com/search?&query=${encodeURIComponent(query)}`;
|
||||
const bookUrl = (id: string) =>
|
||||
`https://www.goodreads.com/book/show/${encodeURIComponent(id)}`;
|
||||
|
||||
const search = async (title: string): Promise<BookInfoShort[]> => {
|
||||
try {
|
||||
const url = searchUrl(title);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(
|
||||
`Failed to search goodreads: ${response.statusText} (${response.status})`,
|
||||
);
|
||||
console.log(`url: ${url}`);
|
||||
throw new Error("Http request failed");
|
||||
}
|
||||
|
||||
const $ = cheerio.load(await response.text());
|
||||
|
||||
const resultElements = $(`tr[itemtype="http://schema.org/Book"]`).toArray();
|
||||
return resultElements.map((element) => ({
|
||||
title: $(element).find(".bookTitle").text().trim(),
|
||||
author: $(element).find(".authorName").text().trim(),
|
||||
id: $(element).find(".u-anchorTarget").attr("id").trim(),
|
||||
source: "goodreads",
|
||||
}));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new UserError("Failed to search goodreads for book");
|
||||
}
|
||||
};
|
||||
|
||||
const getBook = async (id: string): Promise<BookInfo> => {
|
||||
try {
|
||||
const url = bookUrl(id);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(
|
||||
`Failed to search goodreads: ${response.statusText} (${response.status})`,
|
||||
);
|
||||
console.log(`url: ${url}`);
|
||||
throw new Error("Http request failed");
|
||||
}
|
||||
|
||||
const $ = cheerio.load(await response.text());
|
||||
|
||||
const data = JSON.parse($(`script[type="application/ld+json"]`).text());
|
||||
|
||||
return {
|
||||
title: data.name.trim(),
|
||||
author: data.author[0].name.trim(),
|
||||
description: $(".BookPageMetadataSection__description").text().trim(),
|
||||
id,
|
||||
source: "goodreads",
|
||||
url,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new UserError("Failed to search goodreads for book");
|
||||
}
|
||||
};
|
||||
|
||||
const goodreads: BookSource = {
|
||||
search,
|
||||
getBook,
|
||||
};
|
||||
|
||||
export default goodreads;
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,7 @@
|
|||
import { Client, REST } from "discord.js";
|
||||
|
||||
export type Bot = {
|
||||
client: Client;
|
||||
clientId: string;
|
||||
rest: REST;
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { ChatInputCommandInteraction, SharedSlashCommand } from "discord.js";
|
||||
|
||||
export type Command = {
|
||||
slashCommand: SharedSlashCommand;
|
||||
run(interaction: ChatInputCommandInteraction): Promise<void>;
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import { Command } from "./Command";
|
||||
|
||||
export type CommandHandler = {
|
||||
registerCommand(command: Command): Promise<void>;
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
import { Bot } from "./Bot";
|
||||
import { CommandHandler } from "./CommandHandler";
|
||||
|
||||
export type Module = {
|
||||
init(input: { bot: Bot; commandHandler: CommandHandler }): Promise<void>;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export class UserError extends Error {}
|
|
@ -0,0 +1,27 @@
|
|||
import "dotenv/config";
|
||||
import { Client, Events, GatewayIntentBits, REST } from "discord.js";
|
||||
import { Bot } from "./Bot";
|
||||
|
||||
const createClient = ({
|
||||
token,
|
||||
clientId,
|
||||
}: {
|
||||
token: string;
|
||||
clientId: string;
|
||||
}): Bot => {
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
|
||||
client.once(Events.ClientReady, (readyClient) => {
|
||||
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
|
||||
});
|
||||
|
||||
client.login(token);
|
||||
|
||||
return {
|
||||
client,
|
||||
clientId,
|
||||
rest: new REST({ version: "10" }).setToken(token),
|
||||
};
|
||||
};
|
||||
|
||||
export default createClient;
|
|
@ -0,0 +1,84 @@
|
|||
import { Command } from "./Command";
|
||||
import {
|
||||
ChatInputCommandInteraction,
|
||||
Events,
|
||||
Interaction,
|
||||
Routes,
|
||||
} from "discord.js";
|
||||
import { Bot } from "./Bot";
|
||||
import { CommandHandler } from "./CommandHandler";
|
||||
import { UserError } from "./UserError";
|
||||
|
||||
const registerCommands = async (
|
||||
bot: Bot,
|
||||
commands: Record<string, Command>,
|
||||
) => {
|
||||
try {
|
||||
const commandList = Object.values(commands).map((command) =>
|
||||
command.slashCommand.toJSON(),
|
||||
);
|
||||
|
||||
await bot.rest.put(Routes.applicationCommands(bot.clientId), {
|
||||
body: commandList,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(JSON.stringify(error));
|
||||
}
|
||||
};
|
||||
|
||||
const executeCommand = async (
|
||||
command: Command,
|
||||
interaction: ChatInputCommandInteraction,
|
||||
) => {
|
||||
try {
|
||||
await command.run(interaction);
|
||||
} catch (error) {
|
||||
console.error(`Error executing command ${command.slashCommand.name}`);
|
||||
console.error(error);
|
||||
console.error(JSON.stringify(error));
|
||||
const message =
|
||||
error instanceof UserError
|
||||
? `There was an error executing the command: ${error.message}`
|
||||
: "There was an error executing the command";
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({
|
||||
content: message,
|
||||
ephemeral: true,
|
||||
});
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: message,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createCommandHandler = (bot: Bot): CommandHandler => {
|
||||
const commands: Record<string, Command> = {};
|
||||
|
||||
bot.client.on(Events.InteractionCreate, async (interaction: Interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const commandName = interaction.commandName;
|
||||
const command = commands[commandName];
|
||||
|
||||
if (!command) {
|
||||
console.error(`Command ${commandName} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeCommand(command, interaction);
|
||||
});
|
||||
|
||||
return {
|
||||
async registerCommand(command: Command) {
|
||||
console.log(`Registering command ${command.slashCommand.name}`);
|
||||
commands[command.slashCommand.name] = command;
|
||||
await registerCommands(bot, commands);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default createCommandHandler;
|
|
@ -0,0 +1,52 @@
|
|||
import resolveEnvars from "./resolve-envars";
|
||||
import { describe, it, vi, beforeEach, expect } from "vitest";
|
||||
|
||||
describe(`resolve envars`, () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe(`Given the environment variables exist`, () => {
|
||||
it(`Returns the values`, () => {
|
||||
vi.stubEnv("VAR1", "value1");
|
||||
vi.stubEnv("VAR2", "value2");
|
||||
const { var1, var2 } = resolveEnvars({
|
||||
var1: "VAR1",
|
||||
var2: "VAR2",
|
||||
});
|
||||
expect(var1).toEqual("value1");
|
||||
expect(var2).toEqual("value2");
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Given an environment variable doesn't exist`, () => {
|
||||
it(`Throws an error`, () => {
|
||||
vi.stubEnv("VAR1", "value1");
|
||||
try {
|
||||
resolveEnvars({
|
||||
var1: "VAR1",
|
||||
var2: "VAR2",
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual("Environment variables not set: var2");
|
||||
}
|
||||
expect.assertions(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Given multiple variables don't exist`, () => {
|
||||
it(`Throws an error with all variable names in the message`, () => {
|
||||
try {
|
||||
resolveEnvars({
|
||||
var1: "VAR1",
|
||||
var2: "VAR2",
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Environment variables not set: var1, var2",
|
||||
);
|
||||
}
|
||||
expect.assertions(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
const objectMap = (obj, fn) =>
|
||||
Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v, k)]));
|
||||
|
||||
const objectFilter = (obj, fn) =>
|
||||
Object.fromEntries(Object.entries(obj).filter(([k, v]) => fn(v, k)));
|
||||
|
||||
const resolveEnvars = (
|
||||
vars: Record<string, string>,
|
||||
): Record<string, string> => {
|
||||
const resolvedValues = objectMap(vars, (name: string) => process.env[name]);
|
||||
|
||||
const missingVars = Object.keys(
|
||||
objectFilter(resolvedValues, (value: string) => value === undefined),
|
||||
);
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
throw new Error(`Environment variables not set: ${missingVars.join(", ")}`);
|
||||
}
|
||||
|
||||
return resolvedValues;
|
||||
};
|
||||
|
||||
export default resolveEnvars;
|
16
src/index.ts
16
src/index.ts
|
@ -1 +1,15 @@
|
|||
console.log('hello world')
|
||||
import "dotenv/config";
|
||||
import createClient from "./core/create-bot";
|
||||
import createCommandHandler from "./core/create-command-handler";
|
||||
import resolveEnvars from "./core/resolve-envars";
|
||||
import Books from "./modules/books";
|
||||
|
||||
const { botToken, clientId } = resolveEnvars({
|
||||
botToken: "BOT_TOKEN",
|
||||
clientId: "BOT_CLIENT_ID",
|
||||
});
|
||||
|
||||
const bot = createClient({ token: botToken, clientId });
|
||||
const commandHandler = createCommandHandler(bot);
|
||||
|
||||
Books.init({ bot, commandHandler });
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { Command } from "../../core/Command";
|
||||
import {
|
||||
ActionRow,
|
||||
ActionRowBuilder,
|
||||
Embed,
|
||||
EmbedBuilder,
|
||||
SlashCommandBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder,
|
||||
User,
|
||||
} from "discord.js";
|
||||
|
||||
// TODO inject these
|
||||
import goodreads from "../../book-sources/goodreads";
|
||||
import { BookInfo } from "../../book-sources/BookInfo";
|
||||
import { UserError } from "../../core/UserError";
|
||||
|
||||
const booksearch: Command = {
|
||||
slashCommand: new SlashCommandBuilder()
|
||||
.setName("booksearch")
|
||||
.setDescription("Search for a book")
|
||||
.addStringOption((option) =>
|
||||
option.setName("title").setDescription("Book title").setRequired(true),
|
||||
),
|
||||
|
||||
async run(interaction) {
|
||||
await interaction.deferReply();
|
||||
|
||||
const title = interaction.options.getString("title");
|
||||
const results = await goodreads.search(title);
|
||||
// TODO limit number of results
|
||||
|
||||
if (results.length == 0) {
|
||||
await interaction.reply("No results found");
|
||||
return;
|
||||
}
|
||||
|
||||
const message = results
|
||||
.map(
|
||||
(book) =>
|
||||
`**${book.title}** - *${book.author}* (${book.source}#${book.id})`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
await interaction.editReply(message);
|
||||
},
|
||||
};
|
||||
|
||||
export default booksearch;
|
|
@ -0,0 +1,34 @@
|
|||
import { Command } from "../../core/Command";
|
||||
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
// TODO inject these
|
||||
import goodreads from "../../book-sources/goodreads";
|
||||
|
||||
const getbook: Command = {
|
||||
slashCommand: new SlashCommandBuilder()
|
||||
.setName("getbook")
|
||||
.setDescription("Lookup a book")
|
||||
.addStringOption((option) =>
|
||||
option.setName("id").setDescription("Book id").setRequired(true),
|
||||
),
|
||||
|
||||
async run(interaction) {
|
||||
await interaction.deferReply();
|
||||
|
||||
// TODO this should use a particular source
|
||||
const id = interaction.options.getString("id").split("#")[1];
|
||||
console.log(id);
|
||||
const book = await goodreads.getBook(id);
|
||||
console.log("got book");
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(book.title)
|
||||
.setDescription(book.description)
|
||||
.setAuthor({ name: book.author })
|
||||
.setURL(book.url);
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
|
||||
export default getbook;
|
|
@ -0,0 +1,12 @@
|
|||
import booksearch from "./booksearch";
|
||||
import { Module } from "../../core/Module";
|
||||
import getbook from "./getbook";
|
||||
|
||||
const Books: Module = {
|
||||
async init({ commandHandler }) {
|
||||
await commandHandler.registerCommand(booksearch);
|
||||
await commandHandler.registerCommand(getbook);
|
||||
},
|
||||
};
|
||||
|
||||
export default Books;
|
Loading…
Reference in New Issue