diff --git a/CHANGELOG.md b/CHANGELOG.md index 210c8f6..2e8cf5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,28 @@ # Changelog -## Kaab'ot 1.3.0 (in progress) +## Kaab'ot 1.3.0 ### User -- **`/salat` is a command that can be used to retrieve the Salat timings.** +- **`/5v` is a command that can be used to retrieve Holy Quran commentary from the [Five Volume Commentary](https://www.booksonislam.org/products/five-volume-commentary) collection.** + - Give it a single verse (e.g. `1:1`) and the bot will post the relevant page of the verse into chat. +- **`/salat` is a command that can be used to retrieve Salat timings.** (experimental) - Salat timings are computed from the provided location (city name or precise address). - An optional date can be provided to retrieve Salat timings for a specific day. - Requires a Nominatim instance to be setup. See Technical section for details. + - **Experimental feature.** It must be manually enabled by bot administrators. - `/verse` now accepts the `translations` parameter, which is a comma-separated list of translations to include in the output. - Currently only supports `en`, `ar`, and `ur`. - Translated name of chapter now included in `/verse` outputs. ### Technical -- Bot can now use a Nominatim instance to enable geolocalization features. **Without a Nominatim instance, the `/salat` command (and other geolocalization features) will be disabled.** - - Address to the Nominatim instance must be provided as the `NOMINATIM_URL` environment variable in `.env`. +- Bot can now use a Nominatim instance to enable geolocalization features. This feature is experimental, and not enabled by default. **Without a Nominatim instance, the `/salat` command (and other geolocalization features) will be disabled.** + - Address to the Nominatim instance must be provided as the `geolocalizationUrl` field in `settings.json`. - Read [here](https://github.com/mediagis/nominatim-docker/tree/master/4.4) to learn how to self-host a Nominatim instance within a Docker container. Do note that you need _plenty_ of storage space to store all the data. - -The [public instance](https://kaabot.org) of Kaab'ot uses its own hosted Nominatim instance with full global data, so if you don't feel like hosting your own, just [add](https://add.kaabot.org) the public instance of the bot to your Discord server to spare you the trouble. +- A cache engine is now implemented to memoize lookups wherever possible. Set `cache` to false in `settings.json` if you want to disable this for some reason. +- Console outputs are now more readable. +- Fixed bug with verse numbering. ## Kaab'ot 1.2.0 diff --git a/README.md b/README.md index 9bb85a1..73bf27a 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,11 @@ - Cite verses from the Holy Quran with the `/verse` command. Provides English, Arabic, and Urdu translations automatically. - Analyze the individual Arabic words of each verse by passing the optional `analyse` parameter. - Verses provided from [OpenQuran](https://www.openquran.com/), an easy-to-use Quran search engine. +- Retrieve interesting commentary of the Holy Quran with the `/5v` command. Sources commentary from the [Five Volume Commentary](https://www.booksonislam.org/products/five-volume-commentary) collection (thus `5v`). + - Give it a single verse (e.g. `1:1`) and the bot will post the relevant page of the verse into chat. - On-demand retrieval of Salat timings with the `/salat` command, as well as an opt-in notifier/reminder for prayers with configurable location(s). - Uses [Nominatim](https://github.com/osm-search/Nominatim) for geolocalization. If you are hosting Kaab'ot yourself, you will also have to host your own Nominatim instance. - - **Work in progress.** Currently only available in the nightly version of the bot. + - **Experimental feature.** Must be manually enabled by administrators of self-hosted bots. - Fetch Friday Sermons from [Muslim Television Ahmadiyya International](https://beta.mta.tv/). - Provides both a permalink on MTA **and** a direct download link to a 1920x1080 (full HD) MP4 of the sermon. - Pass `list` parameter to retrieve the last 10 Friday sermons. @@ -82,9 +84,6 @@ DISCORD_BOT_CLIENT="..." # Insert your bot's token here. DISCORD_BOT_SECRET="..." - -# (optional) Nominatim URL for geolocalization. -NOMINATIM_URL="..." ``` Afterwards, run the following commands and the bot will launch automatically: @@ -108,7 +107,7 @@ Not all writings will be retrieved (as not all books are available in PDF format ### Geolocalization setup -To enable use of `/salat` commands (and anything else that supports geolocalization queries), you have to provide a URL to a [Nominatim](https://github.com/osm-search/Nominatim) backend as an environment variable (`NOMINATIM_URL`). The bot will automatically detect the environment variable and enable the relevant commands. +To enable use of `/salat` commands (and anything else that supports geolocalization queries), you have to provide a URL to a [Nominatim](https://github.com/osm-search/Nominatim) backend as the `geolocalizationUrl` field of your `settings.json` file. The bot will automatically detect the setting and enable the relevant commands. The public instance of Kaab'ot uses its own self-hosted backend, but it is _exclusively_ for the use of the public instance (as to avoid overuse from other bots). You will have to host your own if you want to self-host Kaab'ot. If you want to avoid this, just [add](https://add.kaabot.org) the public instance of the bot to your server instead. diff --git a/package.json b/package.json index 69489f3..9add3b6 100644 --- a/package.json +++ b/package.json @@ -12,18 +12,22 @@ "author": "https://github.com/mblouka", "license": "AGPL-3.0-or-later", "dependencies": { + "chalk": "^5.3.0", "chrono-node": "^2.7.5", "dayjs": "^1.11.10", "discord.js": "^14.14.1", "dotenv": "^16.4.5", "html-entities": "^2.5.2", + "jsdom": "^24.0.0", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "node-html-markdown": "^1.3.0", "node-html-parser": "^6.1.12", + "sharp": "^0.33.3", "tz-lookup": "^6.1.25" }, "devDependencies": { + "@types/jsdom": "^21.1.6", "@types/node": "^20.11.25", "esbuild": "^0.20.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f19301..ab7313e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + chalk: + specifier: ^5.3.0 + version: 5.3.0 chrono-node: specifier: ^2.7.5 version: 2.7.5 @@ -23,6 +26,9 @@ importers: html-entities: specifier: ^2.5.2 version: 2.5.2 + jsdom: + specifier: ^24.0.0 + version: 24.0.0 moment: specifier: ^2.30.1 version: 2.30.1 @@ -35,10 +41,16 @@ importers: node-html-parser: specifier: ^6.1.12 version: 6.1.12 + sharp: + specifier: ^0.33.3 + version: 0.33.3 tz-lookup: specifier: ^6.1.25 version: 6.1.25 devDependencies: + '@types/jsdom': + specifier: ^21.1.6 + version: 21.1.6 '@types/node': specifier: ^20.11.25 version: 20.11.25 @@ -76,6 +88,9 @@ packages: resolution: {integrity: sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==} engines: {node: '>=16.11.0'} + '@emnapi/runtime@1.1.1': + resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==} + '@esbuild/aix-ppc64@0.20.1': resolution: {integrity: sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==} engines: {node: '>=12'} @@ -218,6 +233,119 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@img/sharp-darwin-arm64@0.33.3': + resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.3': + resolution: {integrity: sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.2': + resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==} + engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.2': + resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==} + engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.2': + resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.2': + resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.2': + resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.2': + resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.2': + resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.3': + resolution: {integrity: sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.3': + resolution: {integrity: sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.3': + resolution: {integrity: sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.3': + resolution: {integrity: sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.3': + resolution: {integrity: sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.3': + resolution: {integrity: sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.3': + resolution: {integrity: sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.3': + resolution: {integrity: sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.3': + resolution: {integrity: sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [win32] + '@sapphire/async-queue@1.5.2': resolution: {integrity: sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -230,9 +358,15 @@ packages: resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@types/jsdom@21.1.6': + resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==} + '@types/node@20.11.25': resolution: {integrity: sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/ws@8.5.9': resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} @@ -240,13 +374,42 @@ packages: resolution: {integrity: sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chrono-node@2.7.5: resolution: {integrity: sha512-VJWqFN5rWmXVvXAxOD4i0jX8Tb4cLswaslyaAFhxM45zNXPsZleygPbgiaYBD7ORb9fj07zBgJb0Q6eKL+0iJg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} @@ -254,9 +417,37 @@ packages: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} + cssstyle@4.0.1: + resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} + engines: {node: '>=18'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + discord-api-types@0.37.61: resolution: {integrity: sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==} @@ -293,28 +484,78 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-entities@2.5.2: resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + jsdom@24.0.0: + resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + magic-bytes.js@1.10.0: resolution: {integrity: sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + moment-timezone@0.5.45: resolution: {integrity: sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==} moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + node-html-markdown@1.3.0: resolution: {integrity: sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==} engines: {node: '>=10.0.0'} @@ -325,6 +566,58 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nwsapi@2.2.9: + resolution: {integrity: sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==} + + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + + psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.33.3: + resolution: {integrity: sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==} + engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + ts-mixer@6.0.4: resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} @@ -341,6 +634,33 @@ packages: resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} engines: {node: '>=14.0'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + ws@8.14.2: resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} engines: {node: '>=10.0.0'} @@ -353,6 +673,28 @@ packages: utf-8-validate: optional: true + ws@8.17.0: + resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + snapshots: '@discordjs/builders@1.7.0': @@ -402,6 +744,11 @@ snapshots: - bufferutil - utf-8-validate + '@emnapi/runtime@1.1.1': + dependencies: + tslib: 2.6.2 + optional: true + '@esbuild/aix-ppc64@0.20.1': optional: true @@ -473,6 +820,81 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@img/sharp-darwin-arm64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.2 + optional: true + + '@img/sharp-darwin-x64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.2 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.2': + optional: true + + '@img/sharp-linux-arm64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.2 + optional: true + + '@img/sharp-linux-arm@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.2 + optional: true + + '@img/sharp-linux-s390x@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.2 + optional: true + + '@img/sharp-linux-x64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.2 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + optional: true + + '@img/sharp-wasm32@0.33.3': + dependencies: + '@emnapi/runtime': 1.1.1 + optional: true + + '@img/sharp-win32-ia32@0.33.3': + optional: true + + '@img/sharp-win32-x64@0.33.3': + optional: true + '@sapphire/async-queue@1.5.2': {} '@sapphire/shapeshift@3.9.6': @@ -482,22 +904,60 @@ snapshots: '@sapphire/snowflake@3.5.1': {} + '@types/jsdom@21.1.6': + dependencies: + '@types/node': 20.11.25 + '@types/tough-cookie': 4.0.5 + parse5: 7.1.2 + '@types/node@20.11.25': dependencies: undici-types: 5.26.5 + '@types/tough-cookie@4.0.5': {} + '@types/ws@8.5.9': dependencies: '@types/node': 20.11.25 '@vladfrangu/async_event_emitter@2.2.4': {} + agent-base@7.1.1: + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + asynckit@0.4.0: {} + boolbase@1.0.0: {} + chalk@5.3.0: {} + chrono-node@2.7.5: dependencies: dayjs: 1.11.10 + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + css-select@5.1.0: dependencies: boolbase: 1.0.0 @@ -508,8 +968,27 @@ snapshots: css-what@6.1.0: {} + cssstyle@4.0.1: + dependencies: + rrweb-cssom: 0.6.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + dayjs@1.11.10: {} + debug@4.3.4: + dependencies: + ms: 2.1.2 + + decimal.js@10.4.3: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.0.3: {} + discord-api-types@0.37.61: {} discord.js@14.14.1: @@ -582,22 +1061,94 @@ snapshots: fast-deep-equal@3.1.3: {} + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + he@1.2.0: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-entities@2.5.2: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.4: + dependencies: + agent-base: 7.1.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + is-arrayish@0.3.2: {} + + is-potential-custom-element-name@1.0.1: {} + + jsdom@24.0.0: + dependencies: + cssstyle: 4.0.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.9 + parse5: 7.1.2 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.17.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + lodash.snakecase@4.1.1: {} lodash@4.17.21: {} + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + magic-bytes.js@1.10.0: {} + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + moment-timezone@0.5.45: dependencies: moment: 2.30.1 moment@2.30.1: {} + ms@2.1.2: {} + node-html-markdown@1.3.0: dependencies: node-html-parser: 6.1.12 @@ -611,6 +1162,75 @@ snapshots: dependencies: boolbase: 1.0.0 + nwsapi@2.2.9: {} + + parse5@7.1.2: + dependencies: + entities: 4.5.0 + + psl@1.9.0: {} + + punycode@2.3.1: {} + + querystringify@2.2.0: {} + + requires-port@1.0.0: {} + + rrweb-cssom@0.6.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver@7.6.0: + dependencies: + lru-cache: 6.0.0 + + sharp@0.33.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.0 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.3 + '@img/sharp-darwin-x64': 0.33.3 + '@img/sharp-libvips-darwin-arm64': 1.0.2 + '@img/sharp-libvips-darwin-x64': 1.0.2 + '@img/sharp-libvips-linux-arm': 1.0.2 + '@img/sharp-libvips-linux-arm64': 1.0.2 + '@img/sharp-libvips-linux-s390x': 1.0.2 + '@img/sharp-libvips-linux-x64': 1.0.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + '@img/sharp-linux-arm': 0.33.3 + '@img/sharp-linux-arm64': 0.33.3 + '@img/sharp-linux-s390x': 0.33.3 + '@img/sharp-linux-x64': 0.33.3 + '@img/sharp-linuxmusl-arm64': 0.33.3 + '@img/sharp-linuxmusl-x64': 0.33.3 + '@img/sharp-wasm32': 0.33.3 + '@img/sharp-win32-ia32': 0.33.3 + '@img/sharp-win32-x64': 0.33.3 + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + symbol-tree@3.2.4: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + ts-mixer@6.0.4: {} tslib@2.6.2: {} @@ -623,4 +1243,36 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 + universalify@0.2.0: {} + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.0.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + ws@8.14.2: {} + + ws@8.17.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@4.0.0: {} diff --git a/src/api/OpenQuran.ts b/src/api/OpenQuran.ts index 7ce8dc3..13f4513 100644 --- a/src/api/OpenQuran.ts +++ b/src/api/OpenQuran.ts @@ -123,7 +123,7 @@ interface OpenQuranIdiomaticWordInfo { } } -interface OpenQuranIdiomaticVerse { +export interface OpenQuranIdiomaticVerse { /** * Index of the chapter. */ @@ -193,7 +193,7 @@ export async function idiomaticSearch(query: string) { )! formattedResults.push({ - verse: verse.id, + verse: verse.verseNum, chapter: verse.chapterNum, chapterName: { arabic: chapter.name, diff --git a/src/api/Prayer.ts b/src/api/Prayer.ts index f588a5b..e90e013 100644 --- a/src/api/Prayer.ts +++ b/src/api/Prayer.ts @@ -1,6 +1,8 @@ /** May Allah have mercy on my soul for this GARBAGE port. */ import getPrayerTimes from "./ChernobylPrayer/PrayUi" +import settings from "../settings" + interface Geolocalization { lat: string lon: string @@ -8,19 +10,21 @@ interface Geolocalization { } async function geolocalize(query: string) { + const nominatimUrl = (await settings()).geolocalizationUrl + if (nominatimUrl === undefined) { + throw new Error("Geolocalization URL is not provided in settings.json.") + } + const params = new URLSearchParams() params.set("q", query) params.set("format", "jsonv2") // TODO: Self-host. - const request = await fetch( - `${process.env.NOMINATIM_URL}/search?${params.toString()}`, - { - headers: { - "User-Agent": "Kaabot ", - }, - } - ) + const request = await fetch(`${nominatimUrl}/search?${params.toString()}`, { + headers: { + "User-Agent": "Kaabot ", + }, + }) // TODO: Sanity checks on response. return (await request.json()) as readonly Geolocalization[] diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..7128133 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,56 @@ +import { promises as fs, existsSync as exists } from "node:fs" +import path from "node:path" + +import settings from "./settings" +import { type OpenQuranIdiomaticVerse } from "./api/OpenQuran" + +type CacheType = { + image: Buffer + verse: OpenQuranIdiomaticVerse +} + +const memory = {} as Record + +export default { + async setup() { + if ((await settings()).cache && !exists("./.cache")) { + await fs.mkdir("./.cache") + } + }, + + async get(type: T, key: string) { + if (!(await settings()).cache) { + return undefined // Cache is disabled. + } + + if (type === "image") { + const cachefile = path.join("./.cache", key) + if (exists(cachefile)) { + return (await fs.readFile(cachefile)) as CacheType[T] + } + return undefined + } else if (type === "verse") { + return memory[key] as CacheType[T] + } + throw new Error("Guh??") + }, + + async set( + type: T, + key: string, + value: CacheType[T] + ): Promise { + if (!(await settings()).cache) { + return undefined as unknown as CacheType[T] // Cache is disabled. + } + + if (type === "image") { + const cachefile = path.join("./.cache", key) + await fs.writeFile(cachefile, value as Buffer) + return value as CacheType[T] + } else if (type === "verse") { + return (memory[key] = value) as CacheType[T] + } + throw new Error("Guh??") + }, +} diff --git a/src/commands/5v.ts b/src/commands/5v.ts new file mode 100644 index 0000000..da9c548 --- /dev/null +++ b/src/commands/5v.ts @@ -0,0 +1,93 @@ +// MainImage +// https://www.alislam.org/quran/view/?page=0®ion=E51&CR=&verse= + +import { BotCommand } from "." +import cache from "../cache" + +import sharp from "sharp" +import { parse } from "node-html-parser" + +export default { + name: "5v", + description: "Retrieves verse commentary from the five volume commentary.", + options: [ + { + name: "query", + description: "Chapter and verse. E.g., '1:1'.", + type: "string", + required: true, + }, + ], + async command(interaction) { + const query = interaction.options.getString("query", true).trim() + // Pattern match the components of the query. + let [, chapter_str, verse_str] = query.match(/(\d+)[:\s]+(\d+)/) ?? [] + if (chapter_str === undefined) { + throw new Error("Invalid query format.") + } + + // See if we already have the page cached for it. + // This will result in an arbitrary fs lookup - we really trust regex in + // not screwing us over here :v + const cacheId = `5v-${chapter_str}-${verse_str}.jpg` + const cachedBuffer = await cache.get("image", cacheId) + if (cachedBuffer !== undefined) { + await interaction.editReply({ files: [cachedBuffer] }) + return + } + + // Fetch 5v redirect information. + const fvRedirect = await ( + await fetch( + `https://www.alislam.org/quran/view/?page=0®ion=E51&CR=&verse=${chapter_str}:${verse_str}`, + { redirect: "follow" } + ) + ).text() + + let fvPage: string + + // Did alislam.org do a good job? + if (fvRedirect.includes("MainImage")) { + // No additional redirect work necessary. + fvPage = fvRedirect + } else { + // Do the funny page extraction. + const segments = fvRedirect.match(/\d+®ion=E\d+&CR=/) + if (!segments?.[0]) { + throw new Error("Five volume query failed.") + } + + // Fetch page information. + fvPage = await ( + await fetch(`https://www.alislam.org/quran/view/?page=${segments[0]}`) + ).text() + } + + // Parse the webpage. + const fakedom = parse(fvPage) + + // Retrieve the page image. + const mainImagePathname = fakedom + .querySelector("#MainImage") + ?.getAttribute("src") + const mainImageUrl = `https://www.alislam.org${mainImagePathname}` + + // Download the image. + const imageBuffer = await ( + await fetch(mainImageUrl, { redirect: "follow" }) + ).arrayBuffer() + + // Load the image into Sharp, flatten transparency to white background. + const flattenedImage = await sharp(imageBuffer) + .flatten({ + background: { r: 255, g: 255, b: 255 }, + }) + .toBuffer() + + // Post image! + await interaction.editReply({ files: [flattenedImage] }) + + // Write to cache for subsequent lookups. + cache.set("image", cacheId, flattenedImage) + }, +} as BotCommand diff --git a/src/commands/index.ts b/src/commands/index.ts index 4eda644..264c916 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -24,8 +24,9 @@ import salat from "./salat" import sermon from "./sermon" import calendar from "./calendar" import library from "./library" +import fivevol from "./5v" import help from "./help" -export default [source, verse, salat, sermon, calendar, library, help] +export default [source, verse, salat, sermon, calendar, library, fivevol, help] export interface BotCommandOption { readonly name: string diff --git a/src/commands/salat.ts b/src/commands/salat.ts index 2df3ea7..2ccb735 100644 --- a/src/commands/salat.ts +++ b/src/commands/salat.ts @@ -2,6 +2,7 @@ import { BotCommand } from "." import Prayer from "../api/Prayer" import embed from "../embed" import * as chrono from "chrono-node" +import settings from "../settings" export default { name: "salat", @@ -22,9 +23,9 @@ export default { }, ], async command(interaction) { - if (process.env.NOMINATIM_URL === undefined) { + if ((await settings()).geolocalizationUrl === undefined) { throw new Error( - "This bot does not have geolocalization setup. Please contact your bot administrator about this." + "Salat timing calculation is an experimental feature, and must be manually enabled by the bot administrator." ) } diff --git a/src/commands/verse.ts b/src/commands/verse.ts index 8366bcd..d63d990 100644 --- a/src/commands/verse.ts +++ b/src/commands/verse.ts @@ -140,7 +140,7 @@ export default { } else { await interaction.editReply( embed({ - title: `Holy Quran, ${verses[0].chapterName.transliteration} (${verses[0].chapterName.arabic}), Verses ${query}`, + title: `Holy Quran, ${verses[0].chapterName.transliteration} (${verses[0].chapterName.arabic}, "${verses[0].chapterName.english}"), Verses ${query}`, buttons: [ { text: "📖 Open in Quran", diff --git a/src/index.ts b/src/index.ts index 590bb12..11eee71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ */ import "dotenv/config" +import chalk from "chalk" import { Client, @@ -27,11 +28,37 @@ import { RESTPostAPIChatInputApplicationCommandsJSONBody, } from "discord.js" +import cache from "./cache" import version from "./version" import embed from "./embed" import commands, { build } from "./commands" import settings from "./settings" +// Overwrite logging functions for ~style~. +const [oldLog, oldWarn, oldError] = [console.log, console.warn, console.error] +console.log = (fmt: string, ...args: any[]) => + oldLog( + `${chalk.bgGreen(chalk.white("kb"))} ${chalk.bgBlue( + chalk.white("info") + )} ${fmt}`, + ...args + ) +console.warn = (fmt: string, ...args: any[]) => + oldWarn( + `${chalk.bgGreen(chalk.white("kb"))} ${chalk.bgYellow( + chalk.white("warn") + )} ${fmt}`, + ...args + ) + +console.error = (fmt: string, ...args: any[]) => + oldError( + `${chalk.bgGreen(chalk.white("kb"))} ${chalk.bgRed( + chalk.white("err ") + )} ${fmt}`, + ...args + ) + const secret = process.env.DISCORD_BOT_SECRET! // Mode. Used for the nightly bot. @@ -90,16 +117,14 @@ client.on("interactionCreate", async (interaction) => { if (logging?.logCommandInvocations) { const optionsFormatted = interaction.options.data - .map((opt) => `${opt.name}=${opt.value?.toString() ?? "undefined"}`) + .map((opt) => `${opt.name}="${opt.value?.toString() ?? "undefined"}"`) .join(", ") console.log( - `Command "${ - interaction.commandName - }" executing with options ${optionsFormatted} by ${ + `Cmd ${interaction.commandName}(${optionsFormatted}) user=${ interaction.user.id - } (${interaction.user.username}) in guild ${ + }(${interaction.user.username}) guild=${ interaction.guild?.id ?? "undefined" - } (${interaction.guild?.name ?? "undefined"})` + }(${interaction.guild?.name ?? "undefined"})` ) } @@ -154,13 +179,16 @@ client.on("ready", async () => { }) console.log( - `Kaab'ot ${version}${ - mode ? ` (${mode})` : "" - } @ https://kaabot.org\nSource code available at https://github.com/mblouka/kaabot` + `Kaab'ot ${version}${mode ? ` (${mode})` : ""} @ https://kaabot.org` ) +console.log("Source code available at https://github.com/mblouka/kaabot") + // Preload settings. await settings() +// Setup cache. +await cache.setup() + // Start bot! client.login(secret) diff --git a/src/settings.ts b/src/settings.ts index 0ddc0e4..bfab449 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,6 +2,18 @@ import { promises as fs, existsSync as exists } from "node:fs" import path from "node:path" export interface KaabotSettings { + /** + * URL to Nominatim instance for geolocalization features. Experimental. + * Defaults to `undefined`. + */ + readonly geolocalizationUrl?: string + + /** + * Whether to enable the caching engine. Enabling this reduces hits to alislam.org. + * Defaults to `true`. + */ + readonly cache: boolean + /** * Whether the bismillah should be counted as part of the verse numbering. * Defaults to `true`. User can manually turn this off as part of their queries. @@ -38,6 +50,7 @@ export interface KaabotSettings { } const defaultSettings = { + cache: true, countBismillah: true, logging: { logErrors: { @@ -62,10 +75,10 @@ export default async function settings() { ) as Partial _settingsCache = Object.assign({ ...defaultSettings }, loadedSettings) } catch (e) { - console.error(e) - console.log( + console.error( "An error was encountered while loading settings. Using defaults." ) + console.error(e) _settingsCache = defaultSettings } } else { @@ -73,13 +86,13 @@ export default async function settings() { _settingsCache = defaultSettings } - if (!process.env.NOMINATIM_URL) { - console.log( - "⚠️ NOMINATIM_URL environment variable not found. Disabling geolocalization features." + if (!_settingsCache.geolocalizationUrl) { + console.warn( + "Geolocalization features are disabled. Check README.md for more information." ) } else { console.log( - `Geolocalization features enabled. Using Nominatim instance at ${process.env.NOMINATIM_URL}` + `Geolocalization features are enabled. Using Nominatim instance at ${_settingsCache.geolocalizationUrl}.` ) }