Add initial bot

This commit is contained in:
dusk 2024-09-01 17:15:53 +08:00
parent 71e6196d40
commit e1fffa26a5
25 changed files with 1944 additions and 5 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules/ node_modules/
build/ build/
.env

7
.idea/prettier.xml Normal file
View File

@ -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>

25
.swcrc Normal file
View File

@ -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
}

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node
COPY . .
RUN npm ci
#TODO need to grab a bundler to build
ENTRYPOINT npm run dev

351
package-lock.json generated
View File

@ -9,13 +9,17 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^14.15.3" "cheerio": "^1.0.0",
"discord.js": "^14.15.3",
"dotenv": "^16.4.5"
}, },
"devDependencies": { "devDependencies": {
"@swc-node/register": "^1.10.9", "@swc-node/register": "^1.10.9",
"@swc/cli": "^0.4.0", "@swc/cli": "^0.4.0",
"@swc/core": "^1.7.22", "@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": { "node_modules/@ampproject/remapping": {
@ -1848,6 +1852,11 @@
"node": ">=6" "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": { "node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -1951,6 +1960,54 @@
"node": ">= 16" "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": { "node_modules/clone-response": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
@ -1990,6 +2047,15 @@
"node": ">= 0.6" "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": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -2004,6 +2070,32 @@
"node": ">= 8" "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": { "node_modules/debug": {
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
@ -2096,6 +2188,80 @@
"url": "https://github.com/discordjs/discord.js?sponsor" "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": { "node_modules/end-of-stream": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@ -2105,6 +2271,17 @@
"once": "^1.4.0" "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": { "node_modules/esbuild": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@ -2399,6 +2576,24 @@
"url": "https://github.com/sindresorhus/got?sponsor=1" "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": { "node_modules/http-cache-semantics": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -2427,6 +2622,17 @@
"node": ">=16.17.0" "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": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -2696,6 +2902,26 @@
"dev": true, "dev": true,
"optional": 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": { "node_modules/node-gyp-build": {
"version": "4.8.2", "version": "4.8.2",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", "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" "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": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -2823,6 +3060,40 @@
"node": ">=4" "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": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -2933,6 +3204,21 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/pseudomap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "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": { "node_modules/semver": {
"version": "7.6.3", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
@ -3389,6 +3680,12 @@
"url": "https://github.com/sponsors/Borewit" "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": { "node_modules/trim-repeated": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -10,12 +10,16 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^14.15.3" "cheerio": "^1.0.0",
"discord.js": "^14.15.3",
"dotenv": "^16.4.5"
}, },
"devDependencies": { "devDependencies": {
"@swc-node/register": "^1.10.9", "@swc-node/register": "^1.10.9",
"@swc/cli": "^0.4.0", "@swc/cli": "^0.4.0",
"@swc/core": "^1.7.22", "@swc/core": "^1.7.22",
"vitest": "^2.0.5" "prettier": "^3.3.3",
"vitest": "^2.0.5",
"vitest-fetch-mock": "^0.3.0"
} }
} }

View File

@ -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;
};

View File

@ -0,0 +1,6 @@
import { BookInfo, BookInfoShort } from "./BookInfo";
export type BookSource = {
search(title: string): Promise<BookInfoShort[]>;
getBook(id: string): Promise<BookInfo>;
};

View File

@ -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",
}
`);
});
});

View File

@ -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

7
src/core/Bot.ts Normal file
View File

@ -0,0 +1,7 @@
import { Client, REST } from "discord.js";
export type Bot = {
client: Client;
clientId: string;
rest: REST;
};

6
src/core/Command.ts Normal file
View File

@ -0,0 +1,6 @@
import { ChatInputCommandInteraction, SharedSlashCommand } from "discord.js";
export type Command = {
slashCommand: SharedSlashCommand;
run(interaction: ChatInputCommandInteraction): Promise<void>;
};

View File

@ -0,0 +1,5 @@
import { Command } from "./Command";
export type CommandHandler = {
registerCommand(command: Command): Promise<void>;
};

6
src/core/Module.ts Normal file
View File

@ -0,0 +1,6 @@
import { Bot } from "./Bot";
import { CommandHandler } from "./CommandHandler";
export type Module = {
init(input: { bot: Bot; commandHandler: CommandHandler }): Promise<void>;
};

1
src/core/UserError.ts Normal file
View File

@ -0,0 +1 @@
export class UserError extends Error {}

27
src/core/create-bot.ts Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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);
});
});
});

View File

@ -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;

View File

@ -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 });

View File

@ -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;

View File

@ -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;

View File

@ -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;