diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index b2fa6cee..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - node: true, - }, - root: true, - extends: [ - "@lisandra-dev/eslint-config", - "plugin:json/recommended", - ], - rules: { - "@typescript-eslint/ban-ts-comment": "off", - "unused-imports/no-unused-imports": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "off", - } -}; diff --git a/.github/workflows/update_ssh.yaml b/.github/workflows/update_ssh.yaml index aa88dff8..1c6abd0c 100644 --- a/.github/workflows/update_ssh.yaml +++ b/.github/workflows/update_ssh.yaml @@ -21,4 +21,5 @@ jobs: cd dicelette/ git pull pnpm i - pnpm run restart + pnpm run build + pnpm run pm2:restart diff --git a/.npmrc b/.npmrc index 699865ab..f556ca4f 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,5 @@ enable-pre-post-scripts=true use-node-version=20.14.0 +link-workspace-packages=true +prefer-workspace-packages=true +recursive-install=true diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index d539b482..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,525 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. - -## [1.20.1](https://github.com/Dicelette/discord-dicelette/compare/1.20.0...1.20.1) (2024-12-01) - - -### Bug Fixes - -* can't delete capa when " - " in name; ([5b76094](https://github.com/Dicelette/discord-dicelette/commit/5b760944720a7def4840522cfd3c47d39df0aac7)) - -## [1.20.0](https://github.com/Dicelette/discord-dicelette/compare/1.19.1...1.20.0) (2024-12-01) - - -### Features - -* **display:** display evaluated formula ([33e070d](https://github.com/Dicelette/discord-dicelette/commit/33e070dcb9a8e1be750949790dd6b473d48a78fa)) - -## [1.19.1](https://github.com/Dicelette/discord-dicelette/compare/1.19.0...1.19.1) (2024-12-01) - - -### Bug Fixes - -* statistic not found when registering the first time a dice ([498cfa1](https://github.com/Dicelette/discord-dicelette/commit/498cfa1c18479c6abc9ef316c47ff7736a9e4566)) - -## [1.19.0](https://github.com/Dicelette/discord-dicelette/compare/1.18.3...1.19.0) (2024-12-01) - - -### Features - -* it is now possible to remove template dice value from user ([0fa1b67](https://github.com/Dicelette/discord-dicelette/commit/0fa1b6793f1c7135774dabf0b73aa0cdf96780be)) - - -### Bug Fixes - -* logger with wrong config doesn't display logs ([589d8a8](https://github.com/Dicelette/discord-dicelette/commit/589d8a83c0033fc3403d0aed2e06b047ce0f88d6)) -* return default char when using display/graph with no charName ([9f9a54f](https://github.com/Dicelette/discord-dicelette/commit/9f9a54fc5d5ec70e7b0bb7561fd679124012b45c)) - -## [1.18.3](https://github.com/Dicelette/discord-dicelette/compare/1.18.2...1.18.3) (2024-12-01) - - -### Bug Fixes - -* multiple dice combined not recognized properly ([cdd78d0](https://github.com/Dicelette/discord-dicelette/commit/cdd78d0cece78a297338ada38efa484a07fcabd3)) -* no need to verify the template each time user add a new dice skill ([0fa5c3f](https://github.com/Dicelette/discord-dicelette/commit/0fa5c3fb2e5ff20c5f1225fa884b84277dd155a4)) - -## [1.18.2](https://github.com/Dicelette/discord-dicelette/compare/1.18.1...1.18.2) (2024-11-30) - - -### Bug Fixes - -* crash at startup ([795c8e3](https://github.com/Dicelette/discord-dicelette/commit/795c8e37eb89549d80b8efe46cbed86fb85a868b)) - -## [1.18.1](https://github.com/Dicelette/discord-dicelette/compare/1.18.0...1.18.1) (2024-11-30) - - -### Bug Fixes - -* channel where to send character sheet when default channel is not set ([e86c701](https://github.com/Dicelette/discord-dicelette/commit/e86c701fb4c4b2825e5e7721695916057c07b01e)) -* rename 'combination' to 'combinaison' in codebase (main) ([ed9639d](https://github.com/Dicelette/discord-dicelette/commit/ed9639d8183f7ea3aa119ed86267929da3a01768)) - -## [1.18.0](https://github.com/Dicelette/discord-dicelette/compare/1.17.0...1.18.0) (2024-11-25) - - -### Features - -* **register:** allow to wipe all characters when registering new template ([4fe5939](https://github.com/Dicelette/discord-dicelette/commit/4fe5939f90da959b7cf049b4e940e4109a6e5433)) - - -### Bug Fixes - -* **copyResult:** add compare result (failure/success) & adjust aspect with other comments ([f337d1a](https://github.com/Dicelette/discord-dicelette/commit/f337d1a41e68b53c36f3dcca2b475032aa09c498)) - -## [1.17.0](https://github.com/Dicelette/discord-dicelette/compare/1.16.0...1.17.0) (2024-11-16) - - -### Features - -* **copyResult:** add possibility to create link using reaction + button ([7045224](https://github.com/Dicelette/discord-dicelette/commit/7045224459b8c96ee323431771e24bece13012f0)) - - -### Bug Fixes - -* crash when changing command /delete cmd ([a96dd65](https://github.com/Dicelette/discord-dicelette/commit/a96dd65eaf4c45fd494c429e0512ea7882e18713)) -* **dbRoll:** accent should be kept in the final roll message ([eea8876](https://github.com/Dicelette/discord-dicelette/commit/eea88767eec8a91050a41ee69515634c48a30736)) - -## [1.16.0](https://github.com/Dicelette/discord-dicelette/compare/1.15.0...1.16.0) (2024-11-10) - - -### Features - -* **contextMenu:** add a command to copy the result ([88a6948](https://github.com/Dicelette/discord-dicelette/commit/88a694860a622dd6be6f13db60c0b2de6387b2d3)) - - -### Bug Fixes - -* "D" not recognized ([4d82096](https://github.com/Dicelette/discord-dicelette/commit/4d82096ad1d35143bd345bd8559072b0972e6cda)) -* bulk roll with extra space at the start ([64c86ae](https://github.com/Dicelette/discord-dicelette/commit/64c86aeea8d957393b7cf0989d33879775db767a)) -* should delete message input after sending the result ([90df8b5](https://github.com/Dicelette/discord-dicelette/commit/90df8b5404950f7786da647e4d823e633dc18b7e)) - -## [1.15.0](https://github.com/Dicelette/discord-dicelette/compare/1.14.0...1.15.0) (2024-11-02) - - -### Features - -* **delete-char:** now add a wall to prevent deleting by error (confirm button) ([b428938](https://github.com/Dicelette/discord-dicelette/commit/b4289382b26358216f564074d917a2ff14f7f1bf)) - - -### Bug Fixes - -* async reaction ([65d58ed](https://github.com/Dicelette/discord-dicelette/commit/65d58ed56d9f6ba26e4fc17f6165a3fda04e3b38)) -* **style:** trimEnd the last test for finalRes (prevent too much newlines) ([a7e13ed](https://github.com/Dicelette/discord-dicelette/commit/a7e13ed3748090025b2869edc773238d95a62ab9)) - -## [1.14.0](https://github.com/Dicelette/discord-dicelette/compare/1.13.4...1.14.0) (2024-10-25) - - -### Features - -* **dice:** allow comparaison in ; syntax ([6962e85](https://github.com/Dicelette/discord-dicelette/commit/6962e8523047ac37052d730d471bf3ae81aa0a47)) - -## [1.13.4](https://github.com/Dicelette/discord-dicelette/compare/1.13.3...1.13.4) (2024-10-24) - -## [1.13.3](https://github.com/Dicelette/discord-dicelette/compare/1.13.2...1.13.3) (2024-10-24) - - -### Bug Fixes - -* typo that break the bot ([f12a74c](https://github.com/Dicelette/discord-dicelette/commit/f12a74ca169bb4b5f5569f7c83707233d28358c5)) - -## [1.13.2](https://github.com/Dicelette/discord-dicelette/compare/1.13.1...1.13.2) (2024-10-24) - - -### Bug Fixes - -* exploding dice wrongly parsed ([dfe1b23](https://github.com/Dicelette/discord-dicelette/commit/dfe1b234e8175243325ae52eab86c6f9ab6743af)) - -## [1.13.1](https://github.com/Dicelette/discord-dicelette/compare/1.13.0...1.13.1) (2024-10-18) - - -### Bug Fixes - -* uniformize entirely broken… ([816b35e](https://github.com/Dicelette/discord-dicelette/commit/816b35e0b231279e2c0c72fea3fba352fba95307)) -* urgent fix for uniformize ([2adbd84](https://github.com/Dicelette/discord-dicelette/commit/2adbd84831f17349477375adf54c45af46c76878)) - -## [1.13.0](https://github.com/Dicelette/discord-dicelette/compare/1.12.2...1.13.0) (2024-10-15) - - -### Features - -* **reaction:** allow to copy reaction between saved message/interaction + send result to DM if used with 📩 ([c97c7cf](https://github.com/Dicelette/discord-dicelette/commit/c97c7cf7ddcfd06b676ec8c8c7dac24cb868a748)) - -## [1.12.2](https://github.com/Dicelette/discord-dicelette/compare/1.12.1...1.12.2) (2024-10-02) - - -### Bug Fixes - -* **findChara:** forgot to return when a char is found ([caab5d2](https://github.com/Dicelette/discord-dicelette/commit/caab5d2341d351b31fc83b407aa69b2d4b030100)) - -## [1.12.1](https://github.com/Dicelette/discord-dicelette/compare/1.12.0...1.12.1) (2024-10-02) - - -### Bug Fixes - -* **filter:** crash when filter >=25 ([13f2396](https://github.com/Dicelette/discord-dicelette/commit/13f2396e410d856bdad9728fbca744b4647cbfef)) -* **filter:** crash when filter >=25 ([cff1b94](https://github.com/Dicelette/discord-dicelette/commit/cff1b9468969a67dfd405e653654362ad2d745b2)) -* **filter:** forgot normal returns when values <=25 ([7a72ec4](https://github.com/Dicelette/discord-dicelette/commit/7a72ec40c19f672b73742b86c0c4ad0672b5362c)) - -## [1.12.0](https://github.com/Dicelette/discord-dicelette/compare/1.11.1...1.12.0) (2024-08-24) - - -### Features - -* add switching locale & fix locale ([4a81dd9](https://github.com/Dicelette/discord-dicelette/commit/4a81dd99131665a6e3177019a22472d38251f815)) - -## [1.11.1](https://github.com/Dicelette/discord-dicelette/compare/1.11.0...1.11.1) (2024-08-15) - -## [1.11.0](https://github.com/Dicelette/discord-dicelette/compare/1.10.0...1.11.0) (2024-08-15) - - -### Features - -* allow to post directly in a forum and let the bot create a new post in it ([603d26f](https://github.com/Dicelette/discord-dicelette/commit/603d26f24758ff72005a1a54a50c81a56cfe962b)) - - -### Bug Fixes - -* use another regex for capitalization ([5a86bd9](https://github.com/Dicelette/discord-dicelette/commit/5a86bd9b424b621f0a5ca9b2f9f9609669b447f3)) - -## [1.10.0](https://github.com/Dicelette/discord-dicelette/compare/1.9.0...1.10.0) (2024-08-09) - - -### Features - -* allow to hide roll result ([f4d6c6e](https://github.com/Dicelette/discord-dicelette/commit/f4d6c6e8c2ed4b6e19fcef8c72fec5798dc1993b)) - - -### Bug Fixes - -* double code d'erreur dans la commande not found character ([d8b2274](https://github.com/Dicelette/discord-dicelette/commit/d8b22741f7410d5781d751980812c2fa0e2a185e)) -* renaming deleted all chara ([1191ecd](https://github.com/Dicelette/discord-dicelette/commit/1191ecddac5a4abacb5e59c701fe7f3c9a02917e)) - -## [1.9.0](https://github.com/Dicelette/discord-dicelette/compare/1.8.0...1.9.0) (2024-08-05) - - -### Features - -* add edit command ([5417182](https://github.com/Dicelette/discord-dicelette/commit/5417182b54731629f3c574eb6ab889b22201cb46)) - -## [1.8.0](https://github.com/Dicelette/discord-dicelette/compare/1.7.6...1.8.0) (2024-07-28) - - -### Features - -* better autocompleting ([014c464](https://github.com/Dicelette/discord-dicelette/commit/014c4644a68ae88a3370571256b58c2b2ccccad0)) - - -### Bug Fixes - -* colorize json has no call signature ([21e95f1](https://github.com/Dicelette/discord-dicelette/commit/21e95f17955614b8594fd4a692084d1a8421aa1c)) - -## [1.7.6](https://github.com/Dicelette/discord-dicelette/compare/1.7.5...1.7.6) (2024-07-20) - - -### Bug Fixes - -* context should not take the bot as it ([e4b0266](https://github.com/Dicelette/discord-dicelette/commit/e4b0266a92315c947cb642f0504458ea876e136e)) -* error when calling directly by name ([25fb1dd](https://github.com/Dicelette/discord-dicelette/commit/25fb1ddac7242590facbaa923bcc8ae629ed19e4)) -* subtext format ([c8be46e](https://github.com/Dicelette/discord-dicelette/commit/c8be46e1d9ca4d6a7ef34795b31edec42cd0c836)) - -## [1.7.5](https://github.com/Dicelette/discord-dicelette/compare/1.7.4...1.7.5) (2024-07-12) - - -### Bug Fixes - -* adjust message when editing avatar ([504fed0](https://github.com/Dicelette/discord-dicelette/commit/504fed0db5522b7d72217ffcc0d8469e4003fb47)) - -## [1.7.4](https://github.com/Dicelette/discord-dicelette/compare/1.7.3...1.7.4) (2024-07-12) - - -### Bug Fixes - -* permission for edit image ([1fce371](https://github.com/Dicelette/discord-dicelette/commit/1fce37182465581d42eb2f11c2277a642eebf3ef)) - -## [1.7.3](https://github.com/Dicelette/discord-dicelette/compare/1.7.2...1.7.3) (2024-07-12) - - -### Bug Fixes - -* forgot translation between things ([52e6f0d](https://github.com/Dicelette/discord-dicelette/commit/52e6f0d8629cef4a4b2ee205389f95fcba172b4a)) - -## [1.7.2](https://github.com/Dicelette/discord-dicelette/compare/1.7.1...1.7.2) (2024-07-12) - - -### Bug Fixes - -* user shouldn't be mandatory ([1187f36](https://github.com/Dicelette/discord-dicelette/commit/1187f36806f3624bbbbd5e7504e8269564bc159b)) - -## [1.7.1](https://github.com/Dicelette/discord-dicelette/compare/1.7.0...1.7.1) (2024-07-12) - - -### Bug Fixes - -* in some condition, discord image link can be cdn ([0f56296](https://github.com/Dicelette/discord-dicelette/commit/0f56296df635e5d6c974df86ad165c3146c515fd)) - -## [1.7.0](https://github.com/Dicelette/discord-dicelette/compare/1.6.6...1.7.0) (2024-07-12) - - -### Features - -* adjust displaying result ([075e0cc](https://github.com/Dicelette/discord-dicelette/commit/075e0cc743a6227fcabfcae7f0c504cf3738a300)) -* allow to edit avatar of embeds ([58b5f6b](https://github.com/Dicelette/discord-dicelette/commit/58b5f6b35ee515b62bb24025110cabda2ed83c22)) - - -### Bug Fixes - -* displaying of delete after was wrong if default settings is applied ([71df774](https://github.com/Dicelette/discord-dicelette/commit/71df77434845a58353454029ae1d965b1d6198f2)) -* empty message ([567cd2a](https://github.com/Dicelette/discord-dicelette/commit/567cd2ab0bf95c5082dfecb1e08271917694530f)) - -## [1.6.6](https://github.com/Dicelette/discord-dicelette/compare/1.6.5...1.6.6) (2024-06-24) - - -### Bug Fixes - -* crash / unhandled error when message not found ([33cabc2](https://github.com/Dicelette/discord-dicelette/commit/33cabc2d5ef87a18e6c0749317cf7d69ce1018c3)) - -## [1.6.5](https://github.com/Dicelette/discord-dicelette/compare/1.6.4...1.6.5) (2024-06-22) - - -### Bug Fixes - -* available tags crashing ([efb10dd](https://github.com/Dicelette/discord-dicelette/commit/efb10dd5f2b970213633f3c3d585a0807fef6c39)) -* can't remove result_channel ([514417b](https://github.com/Dicelette/discord-dicelette/commit/514417b472750a90fb8c2affe5d9be8bf480345f)) -* catch delete error ([df75694](https://github.com/Dicelette/discord-dicelette/commit/df756940a277e87410eab29b9b94b1fd0d49cf03)) -* throw an error if stats not found on a selected char ([6226433](https://github.com/Dicelette/discord-dicelette/commit/622643399a676d4d7dea135827dabde91ce3a909)) - -## [1.6.4](https://github.com/Dicelette/discord-dicelette/compare/1.6.3...1.6.4) (2024-06-20) - -## [1.6.3](https://github.com/Dicelette/discord-dicelette/compare/1.6.2...1.6.3) (2024-06-20) - - -### Bug Fixes - -* translation context word ([4ab3926](https://github.com/Dicelette/discord-dicelette/commit/4ab3926a2cdc8d4af25f8d7308f2b9a8ba1b3f09)) - -## [1.6.2](https://github.com/Dicelette/discord-dicelette/compare/1.6.1...1.6.2) (2024-06-20) - - -### Bug Fixes - -* change one line ([8712a80](https://github.com/Dicelette/discord-dicelette/commit/8712a80932cce41844577c1944c5d0065510d6db)) - -## [1.6.1](https://github.com/Dicelette/discord-dicelette/compare/1.6.0...1.6.1) (2024-06-20) - - -### Bug Fixes - -* typo ([ad08682](https://github.com/Dicelette/discord-dicelette/commit/ad08682a63b699d69b219773a54c337a07182d17)) - -## [1.6.0](https://github.com/Dicelette/discord-dicelette/compare/1.5.1...1.6.0) (2024-06-20) - - -### Features - -* add commands config anchor ([8246805](https://github.com/Dicelette/discord-dicelette/commit/8246805637ef9695e1317159308d80cbe21921fa)) -* **config:** disable logs + anchor ([026dc9d](https://github.com/Dicelette/discord-dicelette/commit/026dc9d87f047408e203503c6080512da69e7b96)) - - -### Bug Fixes - -* set manager on the wrong name ([f9a4422](https://github.com/Dicelette/discord-dicelette/commit/f9a4422b1d91ff32abdc8f5f8f287d718f90422a)) - -## [1.5.0](https://github.com/Dicelette/discord-dicelette/compare/1.4.3...1.5.0) (2024-06-17) - - -### Features - -* capitalize each words instead of the first ([77a0ea8](https://github.com/Dicelette/discord-dicelette/commit/77a0ea8d8a0a5a4fa9776119430fe42240261518)) - - -### Bug Fixes - -* autorole not added in some condition ([0f92687](https://github.com/Dicelette/discord-dicelette/commit/0f92687ea42b50ab58282f7d37814d8e1c23f996)) -* error should be better to understand ([b4ac915](https://github.com/Dicelette/discord-dicelette/commit/b4ac915ed043b4ea73778ba11f8df5421ad3723e)) - -## [1.4.3](https://github.com/Dicelette/discord-dicelette/compare/1.4.2...1.4.3) (2024-06-16) - - -### Bug Fixes - -* do not mention user in comments ([09a8279](https://github.com/Dicelette/discord-dicelette/commit/09a82795ca1bb2d650852fc0f8156472313050ae)) -* format error when pinging user in comments ([287baa9](https://github.com/Dicelette/discord-dicelette/commit/287baa9397167fd274059ab2d40dd246c7587667)) - -## [1.4.2](https://github.com/Dicelette/discord-dicelette/compare/1.4.1...1.4.2) (2024-06-16) - - -### Bug Fixes - -* optimize updating data ([53696cc](https://github.com/Dicelette/discord-dicelette/commit/53696ccbbc1c2cac4cecc70fbc9c82d5a5b59c89)) - -## [1.4.1](https://github.com/Dicelette/discord-dicelette/compare/1.4.0...1.4.1) (2024-06-16) - -## [1.4.0](https://github.com/Dicelette/discord-dicelette/compare/1.3.0...1.4.0) (2024-06-15) - - -### Features - -* allow to set another channel for user ([32c5123](https://github.com/Dicelette/discord-dicelette/commit/32c51239f326072e1abfb6e3b43e203e0da3da1b)) -* support sending in another channel ([1633f89](https://github.com/Dicelette/discord-dicelette/commit/1633f8952ec8ad5de91c0b2e820e2fca377872b9)) - - -### Bug Fixes - -* allow to import/export with channel id ([a6e1a6a](https://github.com/Dicelette/discord-dicelette/commit/a6e1a6acdadea8ad3c6005dac4f71250843b1567)) -* avatar & channel are not mandatory ([fd0a426](https://github.com/Dicelette/discord-dicelette/commit/fd0a426516caba237be64d5136363d69ec5658aa)) -* delete user ([9948082](https://github.com/Dicelette/discord-dicelette/commit/99480823b25eae92fd43932e6edb000104008a9b)) -* do not delete user when not found (because cache error) ([f283d97](https://github.com/Dicelette/discord-dicelette/commit/f283d974b15aa4d7094ae6b16837a52798acb08f)) -* field can be more than 25 so we need to separate the embed during registrationt oo ([55d705e](https://github.com/Dicelette/discord-dicelette/commit/55d705ed3bdd6d04e38336593a669635f099a3f5)) -* hard cap for embeds ([d0c9b55](https://github.com/Dicelette/discord-dicelette/commit/d0c9b55223ddc584d2a6c1df778d8312feed2cbc)) -* if a custom channel is set, private char must be ignored (+-) ([67c7eaa](https://github.com/Dicelette/discord-dicelette/commit/67c7eaa07044be0318f11746f2e071679ec11985)) -* no data in userEmbed ([332f850](https://github.com/Dicelette/discord-dicelette/commit/332f8507a95a9ce38bcd99882c8f92c4fd1a6c22)) -* verify the url of avatar in case of problem ([1305504](https://github.com/Dicelette/discord-dicelette/commit/1305504df84739e2abc9ca508c7d4ee5f5d56ba4)) - -## [1.3.0](https://github.com/Dicelette/discord-dicelette/compare/1.2.0...1.3.0) (2024-06-15) - - -### Features - -* support to custom avatar URL in charasheet ([3d4fd88](https://github.com/Dicelette/discord-dicelette/commit/3d4fd88ff623bc4cd244321de5c12434400c21ea)) - - -### Bug Fixes - -* capitalization issue on import ([18b50b9](https://github.com/Dicelette/discord-dicelette/commit/18b50b9a6a24d1e1ef1280ba11837932217734af)) -* capitalize for userEmbed fields name ([27e4e8c](https://github.com/Dicelette/discord-dicelette/commit/27e4e8cb77d018094682d4909c1669c1b80f7d65)) -* isPrivate not recognized ([5e043a9](https://github.com/Dicelette/discord-dicelette/commit/5e043a9e273550d5c32ce4ede6ffcaafc132dda9)) - -## [1.2.0](https://github.com/Dicelette/discord-dicelette/compare/1.1.0...1.2.0) (2024-06-14) - - -### Features - -* mention user when using the same channel for roll ([5d97b18](https://github.com/Dicelette/discord-dicelette/commit/5d97b18a131b5ae127007588683d060bc542559f)) - - -### Bug Fixes - -* add an error when the file is not a JSON file ([a9e5125](https://github.com/Dicelette/discord-dicelette/commit/a9e5125004df9d579311602fed6446ced9d4111d)) -* can't register user dice ([df060b5](https://github.com/Dicelette/discord-dicelette/commit/df060b516539cc6310bf850e11efa6688cb828ec)) -* dice not recognized ([a8868ea](https://github.com/Dicelette/discord-dicelette/commit/a8868ea476c01be4d87582e2920fb93799d2d5de)) -* error message ([928d847](https://github.com/Dicelette/discord-dicelette/commit/928d8472559a9f55e723cff14d3fc3f839676d3b)) -* graph generation with minimal value ([65f740a](https://github.com/Dicelette/discord-dicelette/commit/65f740abfdaeda7ca04f226ced8dd7bdbf7b1dde)) -* key not found ([7bf758d](https://github.com/Dicelette/discord-dicelette/commit/7bf758d50794d76d088fdc03844a00f9f7064f2f)) -* make the get Embed translation agnostic ([de9cbeb](https://github.com/Dicelette/discord-dicelette/commit/de9cbebf4a4ff18ab65d28b1b1eec4ca2b756c2c)) -* mention on mjroll ([ce3cc6d](https://github.com/Dicelette/discord-dicelette/commit/ce3cc6d482bb02796d361dc383939ac447bc285e)) -* remove limitation for dice at 25 ; ([f8aa815](https://github.com/Dicelette/discord-dicelette/commit/f8aa8158c81fe454b2b76947365e957173861ab4)) -* use primarry the language of the serv, not the registerer ([b77ea9b](https://github.com/Dicelette/discord-dicelette/commit/b77ea9b0110ebaf513b661a37bf97e08979b749a)) -* user not found ([ecbeb00](https://github.com/Dicelette/discord-dicelette/commit/ecbeb000e5a9c08ca9c0ccf5563f2f5e5ef49d4b)) -* user not found ([ed31185](https://github.com/Dicelette/discord-dicelette/commit/ed31185f2b09126c94816b4cc51d46f7b2e0a41e)) - -## [1.1.0](https://github.com/Dicelette/discord-dicelette/compare/1.0.0...1.1.0) (2024-05-18) - - -### Features - -* Bulk_add multiple characters ([4a116cb](https://github.com/Dicelette/discord-dicelette/commit/4a116cb6d3a087bf445df987e00528e07f3da31c)) -* import csv & hide characters ([5089cec](https://github.com/Dicelette/discord-dicelette/commit/5089cec8b12f191bb97966b00babe93159dc8639)) -* private char ([7a5a26d](https://github.com/Dicelette/discord-dicelette/commit/7a5a26dcec86cfcd579716ecc5eb604e7f82c473)) - - -### Bug Fixes - -* excel je te hais ([e676102](https://github.com/Dicelette/discord-dicelette/commit/e67610263393ab34e42ad29513ab1a36269cabe2)) -* forgot opt ([74f82e3](https://github.com/Dicelette/discord-dicelette/commit/74f82e33f26512edd9d0414d475c89c9bb770430)) -* rename commands ([4bed704](https://github.com/Dicelette/discord-dicelette/commit/4bed7046f0c2194106ee1e1a2d992e4357c60630)) -* test ([ea1a5f7](https://github.com/Dicelette/discord-dicelette/commit/ea1a5f764561cad3086d7bb531ddae245bbd873e)) - -## 1.0.0 (2024-04-21) - - -### Features - -* add assets ([b3141a1](https://github.com/Dicelette/discord-dicelette/commit/b3141a149fcd9f87ac656609d0bfb0a9484f2d6e)) -* add auto-role ([04eae4d](https://github.com/Dicelette/discord-dicelette/commit/04eae4de43e025c7fc312ef9ca34094a0cc43aa6)) -* add more options with statistiques ([1fc924c](https://github.com/Dicelette/discord-dicelette/commit/1fc924c3d844135c58428ad497b64baee6e541f1)) -* add tests ([ec8ec70](https://github.com/Dicelette/discord-dicelette/commit/ec8ec70a864fc1e5f7fa3428ed036314eae381dc)) -* add translation ([43f203b](https://github.com/Dicelette/discord-dicelette/commit/43f203b54362824d4eb40c4b467a44db8da65abe)) -* adding a role for dbd / dbroll ([335d573](https://github.com/Dicelette/discord-dicelette/commit/335d57349444d082e5b26e1be7dc07ad18fd0a6e)) -* adding translation ([92b4fe6](https://github.com/Dicelette/discord-dicelette/commit/92b4fe645fb9ee59b883fc8277ff957eac1087e9)) -* allow better handling combination of maths and test value ([2c7368c](https://github.com/Dicelette/discord-dicelette/commit/2c7368c58b4c0f6c10a8b567ca6cb0db01ac2b79)) -* allow bracket command ([70a4a02](https://github.com/Dicelette/discord-dicelette/commit/70a4a02c2284dcd5db98dbf51149b788a3b8d387)) -* allow calcultation in <(value) ([e7630cb](https://github.com/Dicelette/discord-dicelette/commit/e7630cb42d33016bea676f4c13495cfa66acedfc)) -* allow critical success/failure message ([f0f233c](https://github.com/Dicelette/discord-dicelette/commit/f0f233ce9e8691cf4080eb40c3ca4f9c69373b1f)) -* allow multiple logs for forum ([0485125](https://github.com/Dicelette/discord-dicelette/commit/0485125a4bd4e9c6e7e3fabbf525589cf31b020d)) -* allow to close scene & return to the "default" thread channel ([00958f4](https://github.com/Dicelette/discord-dicelette/commit/00958f4dac7cc5f2153a7727b8ee88b55f4d93dc)) -* allow to disable thread auto creation ([6af503d](https://github.com/Dicelette/discord-dicelette/commit/6af503df6e40bc3334bbdad5877625c0d180926d)) -* allow to get the comments from a simple message after dice roll (without comments notation) ([499dc15](https://github.com/Dicelette/discord-dicelette/commit/499dc15c4e58c17f6fad085ea5ed395c208984e1)) -* allow to set a specific channel/thread for rolling result ([f4da2af](https://github.com/Dicelette/discord-dicelette/commit/f4da2af9f6b87a60ccf6e7e471b3cfc14d29017e)) -* allow to use normal message in roll (instead of comments only) ([45e9690](https://github.com/Dicelette/discord-dicelette/commit/45e96907ef9f903d72705029be1b320fb494b161)) -* allow usage of formula ([4c03a08](https://github.com/Dicelette/discord-dicelette/commit/4c03a087217a3002bda51d57ed328daae99e7ff7)) -* allow using directly message to roll ([6e233db](https://github.com/Dicelette/discord-dicelette/commit/6e233db1149dfdd2a7c039a9ba99bf2563811ef1)) -* allow using in forum for forum roleplay ([e5a21c3](https://github.com/Dicelette/discord-dicelette/commit/e5a21c38a8d9c0470f572f17f5ff714219509ba9)) -* alow to delete char if not delete when message was deleted ([517a1ca](https://github.com/Dicelette/discord-dicelette/commit/517a1ca69bc60608bac17ae5b916ff34d1537b2e)) -* better commands for temp bubble ([244ac6f](https://github.com/Dicelette/discord-dicelette/commit/244ac6f13f0f63d1949b134bdf36523ab5a6ffb8)) -* better message ([045d206](https://github.com/Dicelette/discord-dicelette/commit/045d206f4978b8979b9e5a4582e433587ed7a40c)) -* command to display a chara data ([48cc8ff](https://github.com/Dicelette/discord-dicelette/commit/48cc8ff20a1e43b4a52fc45a43debf81ea6ad3be)) -* delete message about thread creation if any ([13a9da4](https://github.com/Dicelette/discord-dicelette/commit/13a9da46303c6035e835178f61458274d69be286)) -* edit will be done by button & modals ([809d137](https://github.com/Dicelette/discord-dicelette/commit/809d13795d33d0821ff9b2f433b4a6339f2d343f)) -* if a channel is named with `🎲` send direct result ([ec8162d](https://github.com/Dicelette/discord-dicelette/commit/ec8162d40ef0c3988f1f23086669f07e45407d2e)) -* link to original when rp message ([9b9a37c](https://github.com/Dicelette/discord-dicelette/commit/9b9a37c03b144c9fcf7b4a825c8a92ee85e92406)) -* register template statistiques ([170ecda](https://github.com/Dicelette/discord-dicelette/commit/170ecdae414eed0613233bfa171684484990d5e5)) -* set a default tags for forum post ([d6cc887](https://github.com/Dicelette/discord-dicelette/commit/d6cc8872c19fcdf77da2bcad2e8254dd8ebaddae)) -* set translation ([e65efa0](https://github.com/Dicelette/discord-dicelette/commit/e65efa07d14b2fe1fb17531f67982924f3ee04ed)) -* template assets ([14821f1](https://github.com/Dicelette/discord-dicelette/commit/14821f15476b6867f53a0c50644dd160dc801e96)) -* verify combination/formula before register ([f645449](https://github.com/Dicelette/discord-dicelette/commit/f645449f53d737e86c3694e966f437910a9cf683)) - - -### Bug Fixes - -* a simple dice always comparate to the stats, even when we don't want to ([c42abcf](https://github.com/Dicelette/discord-dicelette/commit/c42abcf651e64ae459dde34ce8c65be71ccacd75)) -* adjust error throw ([27d20c4](https://github.com/Dicelette/discord-dicelette/commit/27d20c44abb07fe980b1669511eb56092efe2021)) -* adjust help ([879880e](https://github.com/Dicelette/discord-dicelette/commit/879880e87cc92bb3c950ec14765776cd8ab6061b)) -* better eval and message ([26b9439](https://github.com/Dicelette/discord-dicelette/commit/26b9439d95e9f1cac0a15b0088295a3739772201)) -* better name for thread ([934ac15](https://github.com/Dicelette/discord-dicelette/commit/934ac1502cb61ede2a7cecc8a8c5e9f7f876f394)) -* broken indirect roll ([329bf6b](https://github.com/Dicelette/discord-dicelette/commit/329bf6b3852d19fc7bfe2be888d94f392f2596f0)) -* broken success, multiple dice values... ([429065b](https://github.com/Dicelette/discord-dicelette/commit/429065bfbc39e8b9951cc3ed3f0bf96d60f913f9)) -* case of channel not found ([e25a776](https://github.com/Dicelette/discord-dicelette/commit/e25a776341ac6aacf9e0b11b16d496c942c79e43)) -* catch all error when needed ([917ecf2](https://github.com/Dicelette/discord-dicelette/commit/917ecf2c4986144459f8c979be3af5b6b2e9f87e)) -* conversion from json to Enmap ([361c468](https://github.com/Dicelette/discord-dicelette/commit/361c468a86e30b784801d796a22bf4471c00ae79)) -* crash ([ee212f7](https://github.com/Dicelette/discord-dicelette/commit/ee212f710eeb409e9a9fb5bbfc75e722331707d2)) -* critical message doesn't work ([d6e1815](https://github.com/Dicelette/discord-dicelette/commit/d6e18159066fae3c829d9b6845cad5ce8836635e)) -* delete semi-direct message ([a672928](https://github.com/Dicelette/discord-dicelette/commit/a6729283c218599b3932e12e7ebc343b523b6705)) -* deletion with multiple ID ([5e6ac3d](https://github.com/Dicelette/discord-dicelette/commit/5e6ac3d2b2ecee8de511e54c1b9623564b9abb56)) -* duplicate message when roll in result_channel ([fcb75dc](https://github.com/Dicelette/discord-dicelette/commit/fcb75dcd326b44f9d42281a89ddee4e6ca5b5b21)) -* duplication of charName ([af4b15b](https://github.com/Dicelette/discord-dicelette/commit/af4b15b0c960cd66b4bb6fc129daf944801d9053)) -* error when generating formula test ([3aa5e5d](https://github.com/Dicelette/discord-dicelette/commit/3aa5e5d319bb6cd0a2f75025e3513edd62be05b6)) -* field not found ([93c8239](https://github.com/Dicelette/discord-dicelette/commit/93c82394ee6fc859cd38418f2344e5325edfa0af)) -* forgot a keys ([024d014](https://github.com/Dicelette/discord-dicelette/commit/024d014e26fba9fb004198ee91616785e66f0c04)) -* forgot some char in dice recognition ([828067d](https://github.com/Dicelette/discord-dicelette/commit/828067dc4235f0b4f8a07831751a2bd19e771204)) -* graph path ([764d1f1](https://github.com/Dicelette/discord-dicelette/commit/764d1f1dd78fb5e93a88f8ad1efe9924e9d44997)) -* keep bracket message because it a rp reply ([188bc05](https://github.com/Dicelette/discord-dicelette/commit/188bc0558db32bbe4e4f734c34a3aa48ad099912)) -* markdown conversion ([b0f6651](https://github.com/Dicelette/discord-dicelette/commit/b0f6651d22a17a25dbb7235d516212d20d620298)) -* modificator + sign result ([b51940f](https://github.com/Dicelette/discord-dicelette/commit/b51940fa0710dd3fdb2372b7eebcda26c26ad037)) -* no user found when registering skill dice ([a6c3031](https://github.com/Dicelette/discord-dicelette/commit/a6c303140c3a5518716e3d5b5f34c3fd6aeae7bd)) -* optimize translation ([114d29c](https://github.com/Dicelette/discord-dicelette/commit/114d29c7d05a4cc5572c8c70a124ca2e23c14afe)) -* parseEmbeds with name ([31d37ab](https://github.com/Dicelette/discord-dicelette/commit/31d37ab256df2f854a07f6c316e79dd78f7d77bf)) -* presentation of message (title case) ([ef5f07e](https://github.com/Dicelette/discord-dicelette/commit/ef5f07e9d3ad50e7ea143bd79d6fa09aa92144df)) -* prevent roll on simple number ([289fff5](https://github.com/Dicelette/discord-dicelette/commit/289fff5d581f149bc5c2d7d2c7c6d11522ad21d6)) -* random was not good, so check was broken ([ec19d29](https://github.com/Dicelette/discord-dicelette/commit/ec19d298ae22215953b45a218a3b238c7aa64edc)) -* register dice in first registering ([8eab2b0](https://github.com/Dicelette/discord-dicelette/commit/8eab2b09f17a96a0299cd57de1be7e5e7b7f7502)) -* registering DB ([18ee649](https://github.com/Dicelette/discord-dicelette/commit/18ee649900477234ff093d8bd6189264d4c753f1)) -* remove log ([bbaeb25](https://github.com/Dicelette/discord-dicelette/commit/bbaeb257dc889650b77d1f51b89d63030cfc18fd)) -* remove logs ([f100735](https://github.com/Dicelette/discord-dicelette/commit/f100735a1c2903f8ddc080ba6a7f09ff5b15329a)) -* remove unused intents ([a0d4bb6](https://github.com/Dicelette/discord-dicelette/commit/a0d4bb6ec35698a61e27d2d8ef4684033c409914)) -* Required options must be placed before non-required options ([4bdb3ac](https://github.com/Dicelette/discord-dicelette/commit/4bdb3aca3ec621faef9007cb50345a5f4e3a64be)) -* roll detect for non roll message ([072179f](https://github.com/Dicelette/discord-dicelette/commit/072179f7430304284523d8421bf02f16c6816a7d)) -* roll parsing ([02bbae4](https://github.com/Dicelette/discord-dicelette/commit/02bbae4ff9606cbcecb19c3a6ffc2e2eb883d382)) -* send directly image from URL for tuto ([d1ca784](https://github.com/Dicelette/discord-dicelette/commit/d1ca784e524a88b3bcf33c7846cc043fb312dd6b)) -* switch to enmap 6.0.0 ([ad8cdbd](https://github.com/Dicelette/discord-dicelette/commit/ad8cdbd89eecdadb075198550a91ddf1bbf641dd)) -* translation crashed ([7627e90](https://github.com/Dicelette/discord-dicelette/commit/7627e90469bd0ea5ef35ac404f339cf02aa222a7)) -* trim commands ([9919d14](https://github.com/Dicelette/discord-dicelette/commit/9919d141082396cb3d31aef225369530ddc96d09)) -* typo ([a1705a2](https://github.com/Dicelette/discord-dicelette/commit/a1705a2d9b358821dec245e0bae6fbfec7bd49f1)) -* typo in name ([d77c253](https://github.com/Dicelette/discord-dicelette/commit/d77c2539df8fbf5a3d4d035dca12a70343e36e7f)) -* unequal and equal doesn't works ([824758e](https://github.com/Dicelette/discord-dicelette/commit/824758e8fb10f9653c5ebc65575453864547177d)) -* unequal and equal doesn't works ([8fc0be5](https://github.com/Dicelette/discord-dicelette/commit/8fc0be54ef94fc5ec7ab7a79acc152f2971305c9)) -* wrong parsing of comments ([3fec940](https://github.com/Dicelette/discord-dicelette/commit/3fec940cd5bbed242eeffdebcc8e4291b63a2447)) diff --git a/README.md b/README.md index 4d053210..6a7a9a19 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To create your own translation, you need to copy and translate the [`en.ts`](./s > The name must follow the [Discord.js Locale](https://github.com/discordjs/discord-api-types/blob/main/rest/common.ts#L300) > For example, `ChineseCN` for Chinese (China) or `ChineseTW` for Chinese (Taiwan). -You need, after that, to update the [`index.ts`](./src/localizations/index.ts) file to add your translation : +You need, after that, to update the [`database.ts`](./src/localizations/index.ts) file to add your translation : ```ts import newTranslation from "./locales/{translation}"; diff --git a/biome.json b/biome.json index 659d93eb..c9372c09 100644 --- a/biome.json +++ b/biome.json @@ -4,8 +4,7 @@ "ignore": ["node_modules", "dist"] }, "organizeImports": { - "enabled": true, - "ignore": ["src/index.ts"] + "enabled": true }, "linter": { "rules": { @@ -14,6 +13,7 @@ "noVar": "error", "useFilenamingConvention": "error", "useImportType": "error", + "noParameterAssign": "off", "useNamingConvention": { "level": "warn", "options": { @@ -88,17 +88,7 @@ { "include": ["**/*.js"] }, { "include": ["*.json"] }, { - "include": ["src/@types/essential-md.d.ts"], - "linter": { - "rules": { - "suspicious": { - "noExplicitAny": "off" - } - } - } - }, - { - "include": ["src/commands/gimmick/graph.ts"], + "include": ["packages/src/bot/commands/tools/graph.ts"], "linter": { "rules": { "style": { diff --git a/db-management.ts b/db-management.ts deleted file mode 100644 index faef9c55..00000000 --- a/db-management.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type { Settings } from "./src/interfaces/discord"; -import color from "ansi-colors"; -import { Command, Option, type OptionValues } from "commander"; -import Enmap from "enmap"; -import { writeFileSync } from "node:fs"; -import { colorize as colorizeJson } from "json-colorizer"; -import "uniformize"; - -//extends console to add console.error with color -// biome-ignore lint/suspicious/noExplicitAny: -const error = (message?: any, ...optionalParams: any[]) => { - console.error("❌", color.red.bold(message), ...optionalParams); -}; - -// biome-ignore lint/suspicious/noExplicitAny: -const success = (message?: any, ...optionalParams: any[]) => { - console.log("✅", color.green(message), optionalParams); -}; - -// biome-ignore lint/suspicious/noExplicitAny: -const header = (message?: any) => { - return color.bold.underline(message); -}; - -const myParseArray = (value: string) => { - return value.includes(",") ? value.split(/, ?/) : value; -}; - -const program = new Command(); -program - .addOption(new Option("-g, --guild ", "Guild ID to manage")) - .addOption(new Option("-u, --user ", "User ID to manage")) - .addCommand( - new Command("get").description("Get data from the database").action(function ( - this: Command - ) { - // Add type annotation for 'this' - getData(this.optsWithGlobals()); - }) - ) - .addCommand( - new Command("delete").description("Delete data from the database").action(function ( - this: Command - ) { - deleteData(this.optsWithGlobals()); - }) - ) - .addCommand( - new Command("edit-guild") - .description("Edit guild data in the database") - .addOption(new Option("-k, --key ", "Key to set")) - .addOption(new Option("-v, --value ", "Value to set")) - .action(function (this: Command) { - const opt = this.optsWithGlobals(); - editGuildData(opt.guild, opt.key, opt.value); - }) - ) - .addCommand( - new Command("edit-user") - .description("Edit user data in the database") - .addOption(new Option("-k, --key ", "Key to set")) - .addOption(new Option("-v, --value ", "Value to set")) - .addOption(new Option("-c, --charName ", "Character name to edit")) - .action(function (this: Command) { - const options = this.optsWithGlobals(); - editUserData( - options.guild, - options.user, - options.key, - options.value, - options.charName - ); - }) - ); - -const db = new Enmap({ - name: "settings", -}) as Settings; - -program.parse(); - -function deleteAnUser(userId: string) { - const entries = db.entries(); - if (Object.entries(entries).length === 0) { - error("No data found."); - return; - } - for (const [key, value] of entries) { - if (value?.user?.[userId]) { - db.delete(key, `user.${userId}`); - success(`Deleted data for user ${userId} in guild ${key}`); - } - } -} - -function readAll() { - const entries = db.entries(); - if (Object.entries(entries).length === 0) { - error("No data found."); - return; - } - for (const [key, value] of entries) { - console.log(`Guild id: ${header(`${key}`)}`); - console.log(colorizeJson(JSON.stringify(value, null, 2))); - } -} - -function getGuild(guildId: string) { - const guild = db.get(guildId); - console.log("Guild data for :", header(guildId)); - if (!guild) { - error(`No data found for guild ${guildId}`); - return; - } - console.log(colorizeJson(JSON.stringify(guild, null, 2))); -} - -function getDataUser(guildId: string, userId: string) { - const user = db.get(guildId, userId); - console.log("User data for :", header(userId)); - if (!user) { - error(`No data found for user ${userId}`); - return; - } - console.log(colorizeJson(JSON.stringify(user, null, 2))); -} - -function getAllDataForUser(userId: string) { - //search the user in the entire database - const entries = db.entries(); - if (Object.entries(entries).length === 0) { - error("No data found."); - return; - } - for (const [key, value] of entries) { - console.log(`User ${header(userId)} in guild_id ${header(key)}:`); - if (value?.user?.[userId]) { - console.log(colorizeJson(JSON.stringify(value.user[userId], null, 2))); - } - } -} - -function editUser( - guildId: string, - userId: string, - key: "charName" | "messageId" | "damageName", - newValue?: string | string[], - charName?: string -) { - const user = db.get(guildId, `user.${userId}`); - if (!user) { - error(`No data found for user ${userId}`); - return; - } - //find charName in the user data - const dataUser = user.find( - (data) => charName?.standardize() === data?.charName?.standardize() - ); - const dataIndex = user.findIndex( - (data) => charName?.standardize() === data?.charName?.standardize() - ); - if (!dataUser || dataIndex === -1) { - error(`User ${userId} not found in guild ${guildId} for provided data.`); - return; - } - if (key === "damageName") { - if (Array.isArray(newValue)) { - dataUser.damageName = newValue; - } else { - error("damageName should be an array."); - return; - } - } else if (key === "messageId" && Array.isArray(newValue)) { - dataUser[key] = newValue as [string, string]; - } else { - error("Invalid value provided."); - return; - } - db.set(guildId, dataUser, `user.${userId}.${dataIndex}`); - success(`Updated data for user ${userId} in guild ${guildId}`); -} - -function getData(options: OptionValues) { - console.log(options); - if (!options.guild && !options.user) { - readAll(); - } else if (options.guild && !options.user) { - getGuild(options.guild); - } else if (options.guild && options.user) { - getDataUser(options.guild, options.user); - } else if (!options.guild && options.user) { - getAllDataForUser(options.user); - } -} - -function deleteData(options: OptionValues) { - //create a copy of the database before deleting, in case of accidental deletion - writeFileSync("./export.json", db.export()); - - if (options.guild && !options.user) { - db.delete(options.guild); - success(`Deleted data for guild ${options.guild}`); - } else if (options.guild && options.user) { - db.delete(options.guild, `user.${options.user}`); - } else if (!options.guild && options.user) { - //search in the entire database for the user - deleteAnUser(options.user); - } -} - -function editGuildData(guildId: string, key: string, value: string) { - if (!db.has(guildId)) { - error(`No data found for guild ${guildId}`); - return; - } - db.set(guildId, value, key); - success(`Updated data for guild ${guildId}`); -} - -function editUserData( - guildId: string, - userId: string, - key: string, - value: string, - charName: string -) { - const arrayValue = myParseArray(value); - if (["charName", "messageId", "damageName"].includes(key)) { - editUser( - guildId, - userId, - key as "charName" | "messageId" | "damageName", - arrayValue, - charName - ); - } else { - error("Invalid key provided."); - } -} diff --git a/package.json b/package.json index 1ea997fc..4eb5336a 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,19 @@ { "name": "dicelette", "version": "1.20.1", - "description": "", - "main": "dist/index.js", - "repository": "", - "type": "module", "engineStrict": true, "private": true, "scripts": { - "prebuild": "cross-os clean", - "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json", - "prestart": "pnpm run build", - "start": "pm2 start dist/src/index.js --node-args='-r tsconfig-paths/register' --name dicelette --log-date-format=\"YYYY-MM-DD HH:mm Z\"", - "start:tsx": "pm2 start tsx src/index.ts --name dicelette --log-date-format=\"YYYY-MM-DD HH:mm Z\"", - "start:node": "node -r tsconfig-paths/register dist/src/index.js", - "stop": "pm2 stop dicelette", - "prerestart": "pnpm run build", - "restart": "pm2 restart dicelette", - "delete": "pm2 delete dicelette", - "dev": "tsx watch src/index.ts", - "lint": "pnpm biome format --write src/", - "db": "tsx db-management.ts", + "lint": "biome format --write packages", + "build": "pnpm run --recursive build", + "predev": "pnpm run --recursive prebuild", + "dev": "tsx watch --tsconfig=tsconfig.dev.json --clear-screen=false packages/bot/index.ts ", + "prerelease": "tsc --project tsconfig.json --skipLibCheck --noEmit && tsc-alias -p tsconfig.json", "release": "commit-and-tag-version", - "test": "vitest run", - "test:watch": "vitest", - "prerelease": "tsc --project tsconfig.json --skipLibCheck --noEmit && tsc-alias -p tsconfig.json" - }, - "cross-os": { - "clean": { - "linux": "rm -rf dist", - "win32": "rmdir /s /q dist" - } + "pm2:start": "pm2 start packages/bot/dist/index.js --name dicelette --log-date-format=\"YYYY-MM-DD HH:mm Z\"", + "pm2:stop": "pm2 stop dicelette", + "pm2:restart": "pm2 restart dicelette", + "pm2:delete": "pm2 delete dicelette" }, "engines": { "node": "^20.0.0" @@ -41,47 +24,25 @@ "keywords": [], "author": "", "license": "GNU GPLv3", - "dependencies": { - "@dice-roller/rpg-dice-roller": "^5.5.0", - "@dicelette/core": "^1.5.1", - "@discordjs/rest": "^2.4.0", - "@types/papaparse": "^5.3.14", - "@types/parse-color": "^1.0.3", - "chart.js": "^3.9.1", - "chartjs-node-canvas": "^4.1.6", - "csv-generate": "^4.4.1", - "discord-api-types": "^0.37.104", - "discord.js": "^14.16.3", - "dotenv": "^16.4.5", - "enmap": "^6.0.3", - "i18next": "^24.0.2", - "mathjs": "^14.0.0", - "module-alias": "^2.2.3", - "moment": "^2.30.1", - "node-fetch": "^3.3.2", - "papaparse": "^5.4.1", - "parse-color": "^1.0.0", - "ts-dedent": "^2.2.0", - "tslog": "^4.9.3", - "uniformize": "^2.2.0", - "zod": "^3.23.8" - }, "devDependencies": { - "@biomejs/biome": "1.9.3", - "ansi-colors": "^4.1.3", - "commander": "^12.1.0", - "commit-and-tag-version": "^12.4.4", - "cross-env": "^7.0.3", - "cross-os": "^1.5.0", - "json-colorizer": "^3.0.1", - "ts-node": "^10.9.2", + "@biomejs/biome": "^1.9.4", + "i18next": "^24.0.5", + "rimraf": "^6.0.1", + "ts-loader": "^9.5.1", "tsc-alias": "^1.8.10", "tsconfig-paths": "^4.2.0", - "tscpaths": "^0.0.9", - "tslib": "^2.6.3", - "tsx": "^4.19.1", - "typescript": "5.7.2", - "vite-tsconfig-paths": "^5.0.1", - "vitest": "^2.0.3" + "typescript": "^5.7.2", + "vite-tsconfig-paths": "^5.1.3", + "vitest": "^2.1.8" + }, + "dependencies": { + "@dicelette/core": "^1.6.0", + "@types/node": "^22.10.1", + "dedent": "^1.5.3", + "discord.js": "^14.16.3", + "dotenv": "^16.4.7", + "tslog": "^4.9.3", + "tsx": "^4.19.2", + "uniformize": "^2.2.0" } } diff --git a/src/@types/i18next.d.ts b/packages/@types/i18next.d.ts similarity index 73% rename from src/@types/i18next.d.ts rename to packages/@types/i18next.d.ts index e805976a..29cf21bb 100644 --- a/src/@types/i18next.d.ts +++ b/packages/@types/i18next.d.ts @@ -1,4 +1,4 @@ -import type { resources } from "@localization"; +import type { resources } from "../localization"; declare module "i18next" { interface CustomTypeOptions { diff --git a/packages/bot/index.ts b/packages/bot/index.ts new file mode 100644 index 00000000..6cd47ecc --- /dev/null +++ b/packages/bot/index.ts @@ -0,0 +1,40 @@ +import { logger } from "@dicelette/utils"; +import dotenv from "dotenv"; +import "uniformize"; +import process from "node:process"; +import { client } from "client"; +import { + onDeleteChannel, + onDeleteMessage, + onDeleteThread, + onError, + onInteraction, + onJoin, + onKick, + onMessageSend, + onReactionAdd, + onReactionRemove, + ready, +} from "event"; +import packageJson from "./package.json" assert { type: "json" }; +dotenv.config({ path: ".env" }); +logger.info("Starting bot..."); +//@ts-ignore +export const VERSION = packageJson.version ?? "/"; +try { + ready(client); + onInteraction(client); + onJoin(client); + onMessageSend(client); + onKick(client); + onDeleteMessage(client); + onDeleteChannel(client); + onDeleteThread(client); + onReactionAdd(client); + onReactionRemove(client); + onError(client); +} catch (error) { + logger.fatal(error); +} + +client.login(process.env.DISCORD_TOKEN); diff --git a/packages/bot/package.json b/packages/bot/package.json new file mode 100644 index 00000000..8dc973de --- /dev/null +++ b/packages/bot/package.json @@ -0,0 +1,39 @@ +{ + "name": "@dicelette/bot", + "version": "1.20.1", + "description": "", + "main": "dist/index.js", + "type": "module", + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc --outdir dist --project tsconfig.json && tsc-alias -p tsconfig.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/Dicelette/discord-dicelette.git" + }, + "private": true, + "dependencies": { + "@dicelette/localization": "workspace:*", + "@dicelette/parse_result": "workspace:*", + "@dicelette/types": "workspace:*", + "@dicelette/utils": "workspace:*", + "@discordjs/rest": "^2.4.0", + "@types/parse-color": "^1.0.3", + "canvas": "^2.8.0", + "chart.js": "3.9.1", + "chartjs-node-canvas": "^4.1.6", + "discord-api-types": "^0.37.104", + "discord.js": "^14.16.3", + "enmap": "^6.0.3", + "parse-color": "^1.0.0" + }, + "devDependencies": { + "@types/papaparse": "^5.3.15", + "dedent": "^1.5.3", + "moment": "^2.30.1", + "nodemon": "^3.1.7", + "papaparse": "^5.4.1", + "tsconfig-paths": "^4.2.0" + } +} diff --git a/packages/bot/src/client.ts b/packages/bot/src/client.ts new file mode 100644 index 00000000..b58f283e --- /dev/null +++ b/packages/bot/src/client.ts @@ -0,0 +1,35 @@ +import type { GuildData } from "@dicelette/types"; +import * as Djs from "discord.js"; +import Enmap from "enmap"; + +export class EClient extends Djs.Client { + public settings: Enmap; + + constructor(options: Djs.ClientOptions) { + super(options); + + this.settings = new Enmap({ + name: "settings", + fetchAll: false, + autoFetch: true, + cloneLevel: "deep", + }); + } +} + +export const client = new EClient({ + intents: [ + Djs.GatewayIntentBits.GuildMessages, + Djs.GatewayIntentBits.MessageContent, + Djs.GatewayIntentBits.Guilds, + Djs.GatewayIntentBits.GuildMembers, + Djs.GatewayIntentBits.GuildMessageReactions, + ], + partials: [ + Djs.Partials.Channel, + Djs.Partials.GuildMember, + Djs.Partials.User, + Djs.Partials.Reaction, + Djs.Partials.User, + ], +}); diff --git a/src/commands/admin/configuration.ts b/packages/bot/src/commands/admin/configuration.ts similarity index 97% rename from src/commands/admin/configuration.ts rename to packages/bot/src/commands/admin/configuration.ts index fb02c04c..09bf9bc2 100644 --- a/src/commands/admin/configuration.ts +++ b/packages/bot/src/commands/admin/configuration.ts @@ -1,10 +1,12 @@ -import type { Translation } from "@interfaces/discord"; -import { cmdLn, ln, t } from "@localization"; -import { LocalePrimary, localeList } from "@localization/init"; -import type { EClient } from "@main"; -import { reply } from "@utils"; +// noinspection SuspiciousTypeOfGuard + +import { LocalePrimary, cmdLn, ln, t } from "@dicelette/localization"; +import type { Translation } from "@dicelette/types"; +import type { EClient } from "client"; +import dedent from "dedent"; import * as Djs from "discord.js"; -import { dedent } from "ts-dedent"; +import { localeList } from "locales"; +import { reply } from "messages"; const findLocale = (locale?: Djs.Locale) => { if (locale === Djs.Locale.EnglishUS || locale === Djs.Locale.EnglishGB) @@ -17,6 +19,7 @@ const findLocale = (locale?: Djs.Locale) => { if (name) return LocalePrimary[name as keyof typeof LocalePrimary]; return undefined; }; + export const configuration = { data: new Djs.SlashCommandBuilder() .setName(t("config.name")) @@ -490,7 +493,7 @@ async function display( const guildSettings = client.settings.get(interaction.guild!.id); if (!guildSettings) return; - const dpTitle = (content?: string, toUpperCase?: boolean) => { + const dpTitle = (content: string, toUpperCase?: boolean) => { if (toUpperCase) return `- **__${ul(content)?.capitalize()}__**${ul("common.space")}:`; return `- **__${ul(content)}__**${ul("common.space")}:`; @@ -501,7 +504,7 @@ async function display( if (!settings) return ul("common.no"); if (typeof settings === "boolean") return ul("common.yes"); if (typeof settings === "number") { - if (settings === 0 || guildSettings.disableThread) return ul("common.no"); + if (settings === 0 || guildSettings?.disableThread) return ul("common.no"); return `\`${settings / 1000}\`s (\`${settings / 60000}\`min)`; } if (type === "role") return `<@&${settings}>`; @@ -528,7 +531,7 @@ async function display( ${dpTitle("config.admin.title")} ${dp(guildSettings.logs, "chan")} ${ul("config.admin.desc")} ${dpTitle("config.result.title")} ${dp(guildSettings.rollChannel, "chan")} - ${ul("config.result.desc")} + ${ul("config.result.desc")} ${dpTitle("config.disableThread.title")} ${dp(guildSettings.disableThread)} ${ul("config.disableThread.desc")} ${dpTitle("config.hiddenRoll.title")} ${dp(guildSettings.hiddenRoll, "chan")} diff --git a/src/commands/admin/delete_char.ts b/packages/bot/src/commands/admin/delete_char.ts similarity index 95% rename from src/commands/admin/delete_char.ts rename to packages/bot/src/commands/admin/delete_char.ts index 960e6967..b7ed1db4 100644 --- a/src/commands/admin/delete_char.ts +++ b/packages/bot/src/commands/admin/delete_char.ts @@ -1,12 +1,19 @@ -import { deleteUser } from "@events/on_delete"; -import type { GuildData, PersonnageIds, UserMessageId } from "@interfaces/database"; -import type { DiscordChannel, Translation } from "@interfaces/discord"; -import { cmdLn, ln, t } from "@localization"; -import { logger } from "@logger"; -import type { EClient } from "@main"; -import { embedError, filterChoices, reply, searchUserChannel } from "@utils"; -import { getDatabaseChar } from "@utils/db"; +import { cmdLn, ln } from "@dicelette/localization"; +import type { + DiscordChannel, + GuildData, + PersonnageIds, + UserMessageId, +} from "@dicelette/types"; +import type { Translation } from "@dicelette/types"; +import { filterChoices, logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import { deleteUser, getDatabaseChar } from "database"; import * as Djs from "discord.js"; +import i18next from "i18next"; +import { embedError, reply } from "messages"; +import { searchUserChannel } from "utils"; +export const t = i18next.getFixedT("en"); export const deleteChar = { async autocomplete( diff --git a/src/commands/admin/export.ts b/packages/bot/src/commands/admin/export.ts similarity index 95% rename from src/commands/admin/export.ts rename to packages/bot/src/commands/admin/export.ts index b3a53733..7db67b08 100644 --- a/src/commands/admin/export.ts +++ b/packages/bot/src/commands/admin/export.ts @@ -2,13 +2,12 @@ * Allow to export all characters from the database to a CSV file */ -import { cmdLn, ln, t } from "@localization"; -import type { EClient } from "@main"; -import { getUserFromMessage } from "@utils/db"; - -import type { CSVRow } from "@utils/import_csv"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import type { EClient } from "client"; +import { getUserFromMessage } from "database"; import * as Djs from "discord.js"; import Papa from "papaparse"; +import type { CSVRow } from "utils"; export const exportData = { data: new Djs.SlashCommandBuilder() diff --git a/src/commands/admin/import.ts b/packages/bot/src/commands/admin/import.ts similarity index 93% rename from src/commands/admin/import.ts rename to packages/bot/src/commands/admin/import.ts index 2e428844..e59ea08e 100644 --- a/src/commands/admin/import.ts +++ b/packages/bot/src/commands/admin/import.ts @@ -1,11 +1,16 @@ -import { createDiceEmbed, createStatsEmbed, createUserEmbed } from "@interactions"; -import { cmdLn, ln, t } from "@localization"; -import type { EClient } from "@main"; -import { addAutoRole, reply, repostInThread } from "@utils"; -import { getTemplateWithDB } from "@utils/db"; -import { parseCSV } from "@utils/import_csv"; -import { createEmbedsList } from "@utils/parse"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import type { EClient } from "client"; +import { getTemplateWithDB } from "database"; import * as Djs from "discord.js"; +import { + createDiceEmbed, + createEmbedsList, + createStatsEmbed, + createUserEmbed, + reply, + repostInThread, +} from "messages"; +import { addAutoRole, parseCSV } from "utils"; /** * ! Note: Bulk data doesn't allow to register dice-per-user, as each user can have different dice @@ -14,7 +19,6 @@ import * as Djs from "discord.js"; export const bulkAdd = { data: new Djs.SlashCommandBuilder() .setName(t("import.name")) - .setDMPermission(false) .setDefaultMemberPermissions(Djs.PermissionFlagsBits.ManageRoles) .setNameLocalizations(cmdLn("import.name")) .setDescription(t("import.description")) @@ -146,7 +150,13 @@ export const bulkAdd = { char.channel ?? (char.private && privateChannel ? privateChannel : defaultChannel) ); - addAutoRole(interaction, member.id, !!diceEmbed, !!statsEmbed, client.settings); + await addAutoRole( + interaction, + member.id, + !!diceEmbed, + !!statsEmbed, + client.settings + ); await reply(interaction, { content: ul("import.success", { user: Djs.userMention(member.id) }), }); @@ -166,7 +176,6 @@ export const bulkAdd = { export const bulkAddTemplate = { data: new Djs.SlashCommandBuilder() .setName(t("csv_generation.name")) - .setDMPermission(false) .setDefaultMemberPermissions(Djs.PermissionFlagsBits.ManageRoles) .setNameLocalizations(cmdLn("csv_generation.name")) .setDescription(t("csv_generation.description")) diff --git a/src/commands/admin/index.ts b/packages/bot/src/commands/admin/index.ts similarity index 100% rename from src/commands/admin/index.ts rename to packages/bot/src/commands/admin/index.ts diff --git a/src/commands/admin/template.ts b/packages/bot/src/commands/admin/template.ts similarity index 87% rename from src/commands/admin/template.ts rename to packages/bot/src/commands/admin/template.ts index e00e5c6d..0314e7fb 100644 --- a/src/commands/admin/template.ts +++ b/packages/bot/src/commands/admin/template.ts @@ -6,14 +6,19 @@ import { type StatisticalTemplate, verifyTemplateValue, } from "@dicelette/core"; -import type { GuildData } from "@interfaces/database"; -import { cmdLn, ln, t } from "@localization"; -import { logger } from "@logger"; -import type { EClient } from "@main"; -import { createDefaultThread, downloadTutorialImages, embedError, reply } from "@utils"; -import { bulkDeleteCharacters, bulkEditTemplateUser } from "@utils/parse"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import { type GuildData, TUTORIAL_IMAGES } from "@dicelette/types"; +import { logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import dedent from "dedent"; import * as Djs from "discord.js"; -import { dedent } from "ts-dedent"; +import { + bulkDeleteCharacters, + bulkEditTemplateUser, + createDefaultThread, + embedError, + reply, +} from "messages"; export const generateTemplate = { data: new Djs.SlashCommandBuilder() @@ -229,18 +234,37 @@ export const registerTemplate = { }); return; } + const allowedChannelType = [ + Djs.ChannelType.PublicThread, + Djs.ChannelType.GuildText, + Djs.ChannelType.PrivateThread, + ]; const res = await fetch(template.url).then((res) => res.json()); const templateData = verifyTemplateValue(res); const guildId = interaction.guild.id; - const channel = options.getChannel(t("common.channel"), true); - const publicChannel = options.getChannel(t("register.options.public.name"), false); - const privateChannel = options.getChannel(t("register.options.private.name"), false); + const channel = options.getChannel(t("common.channel"), true, allowedChannelType) as + | Djs.AnyThreadChannel + | Djs.TextChannel; - if ( - (!(channel instanceof Djs.TextChannel) && - !(channel instanceof Djs.ThreadChannel)) || - (!publicChannel && !(channel instanceof Djs.TextChannel)) - ) { + let publicChannel = options.getChannel(t("register.options.public.name"), false); + const privateChannel = options.getChannel(t("register.options.private.name"), false); + if (channel instanceof Djs.TextChannel && !publicChannel) { + publicChannel = await createDefaultThread( + channel, + client.settings, + interaction, + false + ); + } else if (!(channel instanceof Djs.BaseGuildTextChannel) && !publicChannel) { + await reply(interaction, { + embeds: [ + embedError(ul("error.public", { chan: Djs.channelMention(channel.id) }), ul), + ], + ephemeral: true, + }); + return; + } + if (!publicChannel) { await reply(interaction, { embeds: [ embedError(ul("error.public", { chan: Djs.channelMention(channel.id) }), ul), @@ -307,6 +331,17 @@ export const registerTemplate = { value: msgComparator, }); } + if (templateData.customCritical) { + for (const [name, value] of Object.entries(templateData.customCritical)) { + const nameCritical = value.onNaturalDice + ? `(N) ${name.capitalize()}` + : name.capitalize(); + embedTemplate.addFields({ + name: nameCritical, + value: `\`${value.sign}${value.value}\``, + }); + } + } if (templateData.total) embedTemplate.addFields({ name: ul("common.total"), @@ -340,7 +375,7 @@ export const registerTemplate = { ], components: [components], }); - msg.pin(); + await msg.pin(); //save in database file const json = client.settings.get(guildId); @@ -369,22 +404,8 @@ export const registerTemplate = { damageName: damageName ?? [], valid: true, }; - if (publicChannel) json.managerId = publicChannel.id; - else if (interaction.channel instanceof Djs.TextChannel) { - const thread = await createDefaultThread( - interaction!.channel, - client.settings, - interaction, - false - ); - json.managerId = thread.id; - } else { - await reply(interaction, { - embeds: [embedError(ul("error.public"), ul)], - ephemeral: true, - }); - return; - } + json.managerId = publicChannel.id; + if (privateChannel) json.privateChannel = privateChannel.id; client.settings.set(guildId, json); } else { @@ -402,15 +423,25 @@ export const registerTemplate = { }; client.settings.set(guildId, newData); } - await reply(interaction, { content: ul("register.embed.registered"), - files: await downloadTutorialImages(), + files: downloadTutorialImages(), }); - if (options.getBoolean(t("register.options.update.name"))) await bulkEditTemplateUser(client.settings, interaction, ul, templateData); else if (options.getBoolean(t("register.options.delete.name"))) await bulkDeleteCharacters(client.settings, interaction, ul); }, }; + +function downloadTutorialImages() { + const imageBufferAttachments: Djs.AttachmentBuilder[] = []; + for (const url of TUTORIAL_IMAGES) { + const index = TUTORIAL_IMAGES.indexOf(url); + const newMessageAttachment = new Djs.AttachmentBuilder(url, { + name: `tutorial_${index}.png`, + }); + imageBufferAttachments.push(newMessageAttachment); + } + return imageBufferAttachments; +} diff --git a/src/commands/context-menu.ts b/packages/bot/src/commands/context_menus.ts similarity index 95% rename from src/commands/context-menu.ts rename to packages/bot/src/commands/context_menus.ts index e0d9793c..e3afa925 100644 --- a/src/commands/context-menu.ts +++ b/packages/bot/src/commands/context_menus.ts @@ -1,6 +1,6 @@ -import type { Translation } from "@interfaces/discord"; -import { cmdLn, ln, t } from "@localization/index"; -import type { EClient } from "@main"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import type { Translation } from "@dicelette/types"; +import type { EClient } from "client"; import * as Djs from "discord.js"; export const contextMenus = [ diff --git a/packages/bot/src/commands/index.ts b/packages/bot/src/commands/index.ts new file mode 100644 index 00000000..ba310bf4 --- /dev/null +++ b/packages/bot/src/commands/index.ts @@ -0,0 +1,19 @@ +import { ADMIN } from "./admin"; +import { deleteChar } from "./admin/delete_char"; +import { ROLL_AUTO, ROLL_CMDLIST } from "./roll"; +import { GIMMICK, help } from "./tools"; +import newScene from "./tools/new_scene"; +export const autCompleteCmd = [...ROLL_AUTO, ...GIMMICK, deleteChar]; +export const commandsList = [ + ...ROLL_AUTO, + ...ROLL_CMDLIST, + ...GIMMICK, + ...ADMIN, + deleteChar, + help, + newScene, +]; +export * from "./context_menus"; +export * from "./admin"; +export * from "./roll"; +export * from "./tools"; diff --git a/packages/bot/src/commands/roll/base_roll.ts b/packages/bot/src/commands/roll/base_roll.ts new file mode 100644 index 00000000..6afa957d --- /dev/null +++ b/packages/bot/src/commands/roll/base_roll.ts @@ -0,0 +1,47 @@ +import { cmdLn, t } from "@dicelette/localization"; +import type { EClient } from "client"; +import * as Djs from "discord.js"; +import { rollWithInteraction } from "utils"; + +export const diceRoll = { + data: new Djs.SlashCommandBuilder() + .setName(t("roll.name")) + .setNameLocalizations(cmdLn("roll.name")) + .setDescription(t("roll.description")) + .setDescriptionLocalizations(cmdLn("roll.description")) + .addStringOption((option) => + option + .setName(t("roll.option.name")) + .setNameLocalizations(cmdLn("roll.option.name")) + .setDescription(t("roll.option.description")) + .setDescriptionLocalizations(cmdLn("roll.option.description")) + .setRequired(true) + ) + .addBooleanOption((option) => + option + .setName(t("dbRoll.options.hidden.name")) + .setNameLocalizations(cmdLn("dbRoll.options.hidden.name")) + .setDescriptionLocalizations(cmdLn("dbRoll.options.hidden.description")) + .setDescription(t("dbRoll.options.hidden.description")) + .setRequired(false) + ), + async execute(interaction: Djs.CommandInteraction, client: EClient): Promise { + if (!interaction.guild) return; + const channel = interaction.channel; + if (!channel || !channel.isTextBased()) return; + const option = interaction.options as Djs.CommandInteractionOptionResolver; + const dice = option.getString(t("roll.option.name"), true); + const hidden = option.getBoolean(t("dbRoll.options.hidden.name")); + await rollWithInteraction( + interaction, + dice, + channel, + client.settings, + undefined, + undefined, + undefined, + undefined, + hidden + ); + }, +}; diff --git a/src/commands/rolls/dbAtq.ts b/packages/bot/src/commands/roll/dbd.ts similarity index 93% rename from src/commands/rolls/dbAtq.ts rename to packages/bot/src/commands/roll/dbd.ts index 5744d6e8..ee70afd9 100644 --- a/src/commands/rolls/dbAtq.ts +++ b/packages/bot/src/commands/roll/dbd.ts @@ -1,12 +1,12 @@ -import { cmdLn, ln, t } from "@localization"; -import { logger } from "@logger"; -import type { EClient } from "@main"; -import { embedError, filterChoices, reply } from "@utils"; -import { getFirstRegisteredChar, getUserFromMessage, serializeName } from "@utils/db"; -import { rollDice } from "@utils/roll"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import { filterChoices, logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import { getFirstRegisteredChar, getUserFromMessage } from "database"; import * as Djs from "discord.js"; +import { embedError, reply } from "messages"; +import { rollDice, serializeName } from "utils"; -export const dbd = { +export default { data: new Djs.SlashCommandBuilder() .setName(t("rAtq.name")) .setDescription(t("rAtq.description")) @@ -107,7 +107,7 @@ export const dbd = { choices = allCharactersFromUser; } } - if (choices.length === 0) return; + if (!choices || choices.length === 0) return; const filter = filterChoices(choices, interaction.options.getFocused()); await interaction.respond( filter.map((result) => ({ name: result.capitalize(), value: result })) diff --git a/src/commands/rolls/dbroll.ts b/packages/bot/src/commands/roll/dbroll.ts similarity index 91% rename from src/commands/rolls/dbroll.ts rename to packages/bot/src/commands/roll/dbroll.ts index d0542567..d5a7a322 100644 --- a/src/commands/rolls/dbroll.ts +++ b/packages/bot/src/commands/roll/dbroll.ts @@ -1,9 +1,10 @@ -import { cmdLn, ln, t } from "@localization"; -import type { EClient } from "@main"; -import { embedError, filterChoices, reply } from "@utils"; -import { getFirstRegisteredChar, getUserFromMessage, serializeName } from "@utils/db"; -import { rollStatistique } from "@utils/roll"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import { filterChoices } from "@dicelette/utils"; +import type { EClient } from "client"; +import { getFirstRegisteredChar, getUserFromMessage } from "database"; import * as Djs from "discord.js"; +import { embedError, reply } from "messages"; +import { rollStatistique, serializeName } from "utils"; export const dbRoll = { data: new Djs.SlashCommandBuilder() @@ -58,8 +59,7 @@ export const dbRoll = { const options = interaction.options as Djs.CommandInteractionOptionResolver; const focused = options.getFocused(true); const guildData = client.settings.get(interaction.guild!.id); - - if (!guildData) return; + if (!guildData || !guildData.templateID) return; let choices: string[] = []; if (focused.name === t("common.statistic")) { choices = guildData.templateID.statsName; @@ -74,7 +74,7 @@ export const dbRoll = { .map((data) => data.charName ?? "") .filter((data) => data.length > 0); } - if (choices.length === 0) return; + if (!choices || choices.length === 0) return; const filter = filterChoices(choices, interaction.options.getFocused()); await interaction.respond( filter.map((result) => ({ name: result.capitalize(), value: result })) diff --git a/packages/bot/src/commands/roll/index.ts b/packages/bot/src/commands/roll/index.ts new file mode 100644 index 00000000..bbaa4e88 --- /dev/null +++ b/packages/bot/src/commands/roll/index.ts @@ -0,0 +1,7 @@ +import { diceRoll } from "./base_roll"; +import dbd from "./dbd"; +import { dbRoll } from "./dbroll"; +import { mjRoll } from "./mj_roll"; + +export const ROLL_AUTO = [dbRoll, dbd, mjRoll]; +export const ROLL_CMDLIST = [diceRoll]; diff --git a/src/commands/rolls/mj_roll.ts b/packages/bot/src/commands/roll/mj_roll.ts similarity index 95% rename from src/commands/rolls/mj_roll.ts rename to packages/bot/src/commands/roll/mj_roll.ts index 3a11bce1..fa45a9f4 100644 --- a/src/commands/rolls/mj_roll.ts +++ b/packages/bot/src/commands/roll/mj_roll.ts @@ -1,10 +1,12 @@ -import type { UserMessageId } from "@interfaces/database"; -import { cmdLn, ln, t } from "@localization"; -import type { EClient } from "@main"; -import { embedError, filterChoices, reply } from "@utils"; -import { getFirstRegisteredChar, getUserFromMessage, serializeName } from "@utils/db"; -import { rollDice, rollStatistique } from "@utils/roll"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import type { UserMessageId } from "@dicelette/types"; +import { filterChoices } from "@dicelette/utils"; +import type { EClient } from "client"; +import { getFirstRegisteredChar, getUserFromMessage } from "database"; import * as Djs from "discord.js"; +import { embedError, reply } from "messages"; +import { serializeName } from "utils"; +import { rollDice, rollStatistique } from "utils"; export const mjRoll = { data: new Djs.SlashCommandBuilder() @@ -198,7 +200,7 @@ export const mjRoll = { } choices.push(...defaultDice); } - if (choices.length === 0) return; + if (!choices || choices.length === 0) return; const filter = filterChoices(choices, interaction.options.getFocused()); await interaction.respond( filter.map((result) => ({ name: result.capitalize(), value: result })) diff --git a/src/commands/gimmick/display.ts b/packages/bot/src/commands/tools/display.ts similarity index 93% rename from src/commands/gimmick/display.ts rename to packages/bot/src/commands/tools/display.ts index 6aee4b95..e5813f2b 100644 --- a/src/commands/gimmick/display.ts +++ b/packages/bot/src/commands/tools/display.ts @@ -1,14 +1,19 @@ import { generateStatsDice } from "@dicelette/core"; -import { createDiceEmbed, createStatsEmbed } from "@interactions"; -import type { CharacterData } from "@interfaces/database"; -import { cmdLn, findln, ln, t } from "@localization"; -import { logger } from "@logger"; -import type { EClient } from "@main"; -import { embedError, filterChoices, haveAccess, reply } from "@utils"; -import { getDatabaseChar } from "@utils/db"; -import { findChara, findLocation } from "@utils/find"; -import { getEmbeds } from "@utils/parse"; +import { cmdLn, findln, ln, t } from "@dicelette/localization"; +import type { CharacterData } from "@dicelette/types"; +import { filterChoices, logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import { findChara, getDatabaseChar } from "database"; import * as Djs from "discord.js"; +import { + createDiceEmbed, + createStatsEmbed, + embedError, + findLocation, + getEmbeds, + reply, +} from "messages"; +import { haveAccess } from "utils"; export const displayUser = { data: new Djs.SlashCommandBuilder() diff --git a/src/commands/gimmick/edit.ts b/packages/bot/src/commands/tools/edit.ts similarity index 94% rename from src/commands/gimmick/edit.ts rename to packages/bot/src/commands/tools/edit.ts index 27384be5..32603d62 100644 --- a/src/commands/gimmick/edit.ts +++ b/packages/bot/src/commands/tools/edit.ts @@ -1,19 +1,14 @@ -import { deleteUser } from "@events/on_delete"; -import { verifyAvatarUrl } from "@interactions/register/validate"; -import type { - PersonnageIds, - UserMessageId, - UserRegistration, -} from "@interfaces/database"; -import type { DiscordChannel, Translation } from "@interfaces/discord"; -import { cmdLn, findln, ln, t } from "@localization"; -import type { EClient } from "@main"; -import { embedError, filterChoices, haveAccess, reply } from "@utils"; -import { editUserButtons, selectEditMenu } from "@utils/buttons"; -import { getDatabaseChar, registerUser } from "@utils/db"; -import { findLocation } from "@utils/find"; -import { getEmbeds, getEmbedsList } from "@utils/parse"; +import { cmdLn, findln, ln, t } from "@dicelette/localization"; +import type { DiscordChannel } from "@dicelette/types"; +import type { PersonnageIds, UserMessageId, UserRegistration } from "@dicelette/types"; +import type { Translation } from "@dicelette/types"; +import { filterChoices, verifyAvatarUrl } from "@dicelette/utils"; +import type { EClient } from "client"; +import { deleteUser, getDatabaseChar, registerUser } from "database"; import * as Djs from "discord.js"; +import { embedError, findLocation, getEmbeds, getEmbedsList } from "messages"; +import { reply } from "messages"; +import { editUserButtons, haveAccess, selectEditMenu } from "utils"; export const editAvatar = { data: new Djs.SlashCommandBuilder() @@ -346,7 +341,7 @@ export async function move( msgId: oldData.messageId, }; try { - registerUser(userRegister, interaction, client.settings, false, true); + await registerUser(userRegister, interaction, client.settings, false, true); } catch (error) { if ((error as Error).message === "DUPLICATE") await reply(interaction, { embeds: [embedError(ul("error.duplicate"), ul)] }); diff --git a/src/commands/gimmick/graph.ts b/packages/bot/src/commands/tools/graph.ts similarity index 96% rename from src/commands/gimmick/graph.ts rename to packages/bot/src/commands/tools/graph.ts index a99510e3..e3ea4f46 100644 --- a/src/commands/gimmick/graph.ts +++ b/packages/bot/src/commands/tools/graph.ts @@ -1,21 +1,14 @@ import path from "node:path"; -import type { CharacterData, PersonnageIds, UserData } from "@interfaces/database"; -import { cmdLn, ln, t } from "@localization"; -import { logger } from "@logger"; -import type { EClient } from "@main"; -import { - embedError, - filterChoices, - haveAccess, - reply, - searchUserChannel, - sendLogs, -} from "@utils"; -import { getDatabaseChar, getTemplateWithDB, getUserByEmbed } from "@utils/db"; -import { findChara } from "@utils/find"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import type { CharacterData, PersonnageIds, UserData } from "@dicelette/types"; +import { filterChoices, logger } from "@dicelette/utils"; import { ChartJSNodeCanvas } from "chartjs-node-canvas"; +import type { EClient } from "client"; +import { findChara, getDatabaseChar, getTemplateWithDB, getUserByEmbed } from "database"; import * as Djs from "discord.js"; +import { embedError, reply, sendLogs } from "messages"; import parse from "parse-color"; +import { haveAccess, searchUserChannel } from "utils"; async function chart( userData: UserData, @@ -333,7 +326,7 @@ export const graph = { await reply(interaction, { embeds: [embedError(ul("error.generic.e", { e: error as Error }), ul)], }); - sendLogs( + await sendLogs( ul("error.generic.e", { e: error as Error }), interaction.guild, client.settings diff --git a/src/commands/help.ts b/packages/bot/src/commands/tools/help.ts similarity index 96% rename from src/commands/help.ts rename to packages/bot/src/commands/tools/help.ts index 9304753b..252deb5c 100644 --- a/src/commands/help.ts +++ b/packages/bot/src/commands/tools/help.ts @@ -1,10 +1,9 @@ -import { LINKS } from "@interfaces/constant"; -import type { Settings, Translation } from "@interfaces/discord"; -import { cmdLn, ln, t } from "@localization"; -import type { EClient } from "@main"; -import { reply } from "@utils"; +import { cmdLn, ln, t } from "@dicelette/localization"; +import { LINKS, type Settings, type Translation } from "@dicelette/types"; +import type { EClient } from "client"; +import dedent from "dedent"; import * as Djs from "discord.js"; -import { dedent } from "ts-dedent"; +import { reply } from "messages"; export const help = { data: new Djs.SlashCommandBuilder() diff --git a/src/commands/gimmick/index.ts b/packages/bot/src/commands/tools/index.ts similarity index 56% rename from src/commands/gimmick/index.ts rename to packages/bot/src/commands/tools/index.ts index b94a1386..0551a672 100644 --- a/src/commands/gimmick/index.ts +++ b/packages/bot/src/commands/tools/index.ts @@ -3,3 +3,8 @@ import { editAvatar } from "./edit"; import { graph } from "./graph"; export const GIMMICK = [displayUser, graph, editAvatar]; +export * from "./display"; +export * from "./edit"; +export * from "./graph"; +export * from "./help"; +export * from "./new_scene"; diff --git a/src/commands/rolls/base_roll.ts b/packages/bot/src/commands/tools/new_scene.ts similarity index 50% rename from src/commands/rolls/base_roll.ts rename to packages/bot/src/commands/tools/new_scene.ts index f7df3e0d..f5663dee 100644 --- a/src/commands/rolls/base_roll.ts +++ b/packages/bot/src/commands/tools/new_scene.ts @@ -1,59 +1,10 @@ +import { cmdLn, ln, t } from "@dicelette/localization"; +import type { EClient } from "client"; import * as Djs from "discord.js"; +import { deleteAfter, reply, setTagsForRoll } from "messages"; import moment from "moment"; -//group1: Import -import type { EClient } from "@main"; - -//group 2: discord.js -import { deleteAfter, reply, setTagsForRoll } from "@utils"; -import { rollWithInteraction } from "@utils/roll"; - -import { cmdLn, ln, t } from "@localization"; - -export const diceRoll = { - data: new Djs.SlashCommandBuilder() - .setName(t("roll.name")) - .setNameLocalizations(cmdLn("roll.name")) - .setDescription(t("roll.description")) - .setDescriptionLocalizations(cmdLn("roll.description")) - .addStringOption((option) => - option - .setName(t("roll.option.name")) - .setNameLocalizations(cmdLn("roll.option.name")) - .setDescription(t("roll.option.description")) - .setDescriptionLocalizations(cmdLn("roll.option.description")) - .setRequired(true) - ) - .addBooleanOption((option) => - option - .setName(t("dbRoll.options.hidden.name")) - .setNameLocalizations(cmdLn("dbRoll.options.hidden.name")) - .setDescriptionLocalizations(cmdLn("dbRoll.options.hidden.description")) - .setDescription(t("dbRoll.options.hidden.description")) - .setRequired(false) - ), - async execute(interaction: Djs.CommandInteraction, client: EClient): Promise { - if (!interaction.guild) return; - const channel = interaction.channel; - if (!channel || !channel.isTextBased()) return; - const option = interaction.options as Djs.CommandInteractionOptionResolver; - const dice = option.getString(t("roll.option.name"), true); - const hidden = option.getBoolean(t("dbRoll.options.hidden.name")); - await rollWithInteraction( - interaction, - dice, - channel, - client.settings, - undefined, - undefined, - undefined, - undefined, - hidden - ); - }, -}; - -export const newScene = { +export default { data: new Djs.SlashCommandBuilder() .setName(t("scene.name")) .setDescription(t("scene.description")) @@ -91,19 +42,16 @@ export const newScene = { return; } //archive old threads - if ( - channel instanceof Djs.TextChannel || - channel.parent instanceof Djs.ForumChannel || - !channel.name.startsWith("🎲") - ) { - const threads = - channel instanceof Djs.TextChannel - ? channel.threads.cache.filter( - (thread) => thread.name.startsWith("🎲") && !thread.archived - ) - : (channel.parent as Djs.ForumChannel).threads.cache.filter( - (thread) => thread.name === `🎲 ${scene}` && !thread.archived - ); + // noinspection SuspiciousTypeOfGuard + const isTextChannel = channel instanceof Djs.TextChannel; + if (channel.parent instanceof Djs.ForumChannel || !channel.name.startsWith("🎲")) { + const threads = isTextChannel + ? channel.threads.cache.filter( + (thread) => thread.name.startsWith("🎲") && !thread.archived + ) + : (channel.parent as Djs.ForumChannel).threads.cache.filter( + (thread) => thread.name === `🎲 ${scene}` && !thread.archived + ); for (const thread of threads) { await thread[1].setArchived(true); } @@ -114,19 +62,18 @@ export const newScene = { if (threadName.includes("{{date}}")) threadName = threadName.replace("{{date}}", moment().format("DD-MM-YYYY")); - const newThread = - channel instanceof Djs.TextChannel - ? await channel.threads.create({ - name: threadName, - reason: ul("scene.reason"), - }) - : await (channel.parent as Djs.ForumChannel).threads.create({ - name: threadName, - message: { content: ul("scene.reason") }, - appliedTags: [ - (await setTagsForRoll(channel.parent as Djs.ForumChannel)).id as string, - ], - }); + const newThread = isTextChannel + ? await channel.threads.create({ + name: threadName, + reason: ul("scene.reason"), + }) + : await (channel.parent as Djs.ForumChannel).threads.create({ + name: threadName, + message: { content: ul("scene.reason") }, + appliedTags: [ + (await setTagsForRoll(channel.parent as Djs.ForumChannel)).id as string, + ], + }); const threadMention = Djs.channelMention(newThread.id); const msgReply = await reply(interaction, { diff --git a/packages/bot/src/database/delete_user.ts b/packages/bot/src/database/delete_user.ts new file mode 100644 index 00000000..0c851871 --- /dev/null +++ b/packages/bot/src/database/delete_user.ts @@ -0,0 +1,60 @@ +import type { GuildData } from "@dicelette/types"; +import { logger } from "@dicelette/utils"; +import type * as Djs from "discord.js"; +import type Enmap from "enmap"; + +export function deleteUser( + interaction: Djs.CommandInteraction | Djs.ModalSubmitInteraction, + guildData: GuildData, + user?: Djs.User | null, + charName?: string | null +) { + //delete the character from the database + const userCharIndex = guildData.user[user?.id ?? interaction.user.id].findIndex( + (char) => { + return char.charName?.standardize() === charName?.standardize(); + } + ); + if (userCharIndex === -1) { + return guildData; + } + guildData.user[user?.id ?? interaction.user.id].splice(userCharIndex, 1); + return guildData; +} + +export function deleteIfChannelOrThread( + db: Enmap, + guildID: string, + channel: Djs.NonThreadGuildBasedChannel | Djs.AnyThreadChannel +) { + const channelID = channel.id; + cleanUserDB(db, channel); + if (db.get(guildID, "templateID.channelId") === channelID) + db.delete(guildID, "templateID"); + if (db.get(guildID, "logs") === channelID) db.delete(guildID, "logs"); + if (db.get(guildID, "managerId") === channelID) db.delete(guildID, "managerId"); + if (db.get(guildID, "privateChannel") === channelID) + db.delete(guildID, "privateChannel"); + if (db.get(guildID, "rollChannel") === channelID) db.delete(guildID, "rollChannel"); +} + +function cleanUserDB( + guildDB: Enmap, + thread: Djs.GuildTextBasedChannel | Djs.ThreadChannel | Djs.NonThreadGuildBasedChannel +) { + const dbUser = guildDB.get(thread.guild.id, "user"); + if (!dbUser) return; + if (!thread.isTextBased()) return; + /** if private channel was deleted, delete only the private charactersheet */ + + for (const [user, data] of Object.entries(dbUser)) { + const filterChar = data.filter((char) => { + return char.messageId[1] !== thread.id; + }); + logger.silly( + `Deleted ${data.length - filterChar.length} characters for user ${user}` + ); + if (filterChar.length === 0) guildDB.delete(thread.guild.id, `user.${user}`); + else guildDB.set(thread.guild.id, filterChar, `user.${user}`); + } +} diff --git a/packages/bot/src/database/get_template.ts b/packages/bot/src/database/get_template.ts new file mode 100644 index 00000000..4a21c18e --- /dev/null +++ b/packages/bot/src/database/get_template.ts @@ -0,0 +1,57 @@ +import { + type StatisticalTemplate, + templateSchema, + verifyTemplateValue, +} from "@dicelette/core"; +import { ln } from "@dicelette/localization"; +import type { Settings } from "@dicelette/types"; +import * as Djs from "discord.js"; +import type { Message } from "discord.js"; + +/** + * Get the statistical Template using the database templateID information + */ +export async function getTemplateWithDB( + interaction: + | Djs.ButtonInteraction + | Djs.ModalSubmitInteraction + | Djs.CommandInteraction, + enmap: Settings +) { + if (!interaction.guild) return; + const guild = interaction.guild; + const templateID = enmap.get(interaction.guild.id, "templateID"); + const ul = ln(interaction.locale); + if (!enmap.has(interaction.guild.id) || !templateID) + throw new Error(ul("error.noGuildData", { server: interaction.guild.name })); + + const { channelId, messageId } = templateID; + const channel = await guild.channels.fetch(channelId); + if (!channel || channel instanceof Djs.CategoryChannel) return; + try { + const message = await channel.messages.fetch(messageId); + return getTemplate(message, enmap); + } catch (error) { + if ((error as Error).message === "Unknown Message") + throw new Error(ul("error.noTemplateId", { channelId, messageId })); + throw new Error(ul("error.noTemplate")); + } +} + +/** + * Get the guild template when clicking on the "registering user" button or when submitting + */ +export async function getTemplate( + message: Message, + enmap: Settings +): Promise { + const template = message?.attachments.first(); + if (!template) return; + const res = await fetch(template.url).then((res) => res.json()); + if (!enmap.get(message.guild!.id, "templateID.valid")) { + enmap.set(message.guild!.id, true, "templateID.valid"); + return verifyTemplateValue(res); + } + const parsedTemplate = templateSchema.parse(res); + return parsedTemplate as StatisticalTemplate; +} diff --git a/packages/bot/src/database/get_user.ts b/packages/bot/src/database/get_user.ts new file mode 100644 index 00000000..ae615a3a --- /dev/null +++ b/packages/bot/src/database/get_user.ts @@ -0,0 +1,246 @@ +import { findln, ln } from "@dicelette/localization"; +import { parseEmbedToStats, parseTemplateField } from "@dicelette/parse_result"; +import type { + CharDataWithName, + PersonnageIds, + Settings, + Translation, + UserData, +} from "@dicelette/types"; +import type { EClient } from "client"; +import * as Djs from "discord.js"; +import { embedError, ensureEmbed, getEmbeds, parseEmbedFields, reply } from "messages"; +import { haveAccess, searchUserChannel } from "utils"; + +export function getUserByEmbed( + message: Djs.Message, + ul: Translation, + first: boolean | undefined = false, + integrateCombinaison = true, + fetchAvatar = false, + fetchChannel = false +) { + const user: Partial = {}; + const userEmbed = first ? ensureEmbed(message) : getEmbeds(ul, message, "user"); + if (!userEmbed) return; + const parsedFields = parseEmbedFields(userEmbed.toJSON() as Djs.Embed); + const charNameFields = [ + { key: "common.charName", value: parsedFields?.["common.charName"] }, + { key: "common.character", value: parsedFields?.["common.character"] }, + ].find((field) => field.value !== undefined); + if (charNameFields && charNameFields.value !== "common.noSet") { + user.userName = charNameFields.value; + } + const statsFields = getEmbeds(ul, message, "stats")?.toJSON() as Djs.Embed; + user.stats = parseEmbedToStats(parseEmbedFields(statsFields), integrateCombinaison); + const damageFields = getEmbeds(ul, message, "damage")?.toJSON() as Djs.Embed; + const templateDamage = parseEmbedFields(damageFields); + const templateEmbed = first ? userEmbed : getEmbeds(ul, message, "template"); + user.damage = templateDamage; + user.template = parseTemplateField( + parseEmbedFields(templateEmbed?.toJSON() as Djs.Embed) + ); + if (fetchAvatar) user.avatar = userEmbed.toJSON().thumbnail?.url || undefined; + if (fetchChannel) user.channel = message.channel.id; + return user as UserData; +} + +export async function getFirstRegisteredChar( + client: EClient, + interaction: Djs.CommandInteraction, + ul: Translation +) { + const userData = client.settings.get( + interaction.guild!.id, + `user.${interaction.user.id}` + ); + if (!userData) { + await reply(interaction, { + embeds: [embedError(ul("error.notRegistered"), ul)], + ephemeral: true, + }); + return; + } + const firstChar = userData[0]; + const optionChar = firstChar.charName?.capitalize(); + const userStatistique = await getUserFromMessage( + client.settings, + interaction.user.id, + interaction, + firstChar.charName + ); + + return { optionChar, userStatistique }; +} + +/** + * Create the UserData starting from the guildData and using a userId + */ +export async function getUserFromMessage( + guildData: Settings, + userId: string, + interaction: Djs.BaseInteraction, + charName?: string | null, + options?: { + integrateCombinaison?: boolean; + allowAccess?: boolean; + skipNotFound?: boolean; + fetchAvatar?: boolean; + fetchChannel?: boolean; + } +) { + if (!options) + options = { integrateCombinaison: true, allowAccess: true, skipNotFound: false }; + const { integrateCombinaison, allowAccess, skipNotFound } = options; + const ul = ln(interaction.locale); + const guild = interaction.guild; + const user = guildData.get(guild!.id, `user.${userId}`)?.find((char) => { + return char.charName?.subText(charName); + }); + if (!user) return; + const userMessageId: PersonnageIds = { + channelId: user.messageId[1], + messageId: user.messageId[0], + }; + const thread = await searchUserChannel( + guildData, + interaction, + ul, + userMessageId.channelId + ); + if (!thread) throw new Error(ul("error.noThread")); + if (user.isPrivate && !allowAccess && !haveAccess(interaction, thread.id, userId)) { + throw new Error(ul("error.private")); + } + try { + const message = await thread.messages.fetch(userMessageId.messageId); + return getUserByEmbed( + message, + ul, + undefined, + integrateCombinaison, + options.fetchAvatar, + options.fetchChannel + ); + } catch (error) { + if (!skipNotFound) throw new Error(ul("error.user"), { cause: "404 not found" }); + } +} + +export async function getDatabaseChar( + interaction: Djs.CommandInteraction, + client: EClient, + t: Translation, + strict = true +) { + const options = interaction.options as Djs.CommandInteractionOptionResolver; + const guildData = client.settings.get(interaction.guildId as string); + const ul = ln(interaction.locale as Djs.Locale); + if (!guildData) { + await reply(interaction, { embeds: [embedError(ul("error.noTemplate"), ul)] }); + return undefined; + } + const user = options.getUser(t("display.userLowercase")); + let charName = options.getString(t("common.character"))?.toLowerCase(); + if (charName?.includes(ul("common.default").toLowerCase())) charName = undefined; + + if (!user && charName) { + //get the character data in the database + const allUsersData = guildData.user; + const allUsers = Object.entries(allUsersData); + for (const [user, data] of allUsers) { + const userChar = data.find((char) => { + return char.charName?.subText(charName, strict); + }); + if (userChar) { + return { + [user as string]: userChar, + }; + } + } + } + const userData = client.settings.get( + interaction.guild!.id, + `user.${user?.id ?? interaction.user.id}` + ); + const findChara = userData?.find((char) => { + if (charName) return char.charName?.subText(charName, strict); + }); + if (!findChara && charName) { + return undefined; + } + if (!findChara) { + const char = userData?.[0]; + + return char ? { [user?.id ?? interaction.user.id]: char } : undefined; + } + return { + [user?.id ?? interaction.user.id]: findChara, + }; +} + +export async function findChara(charData: CharDataWithName, charName?: string) { + return Object.values(charData).find((data) => { + if (data.charName && charName) { + return data.charName.subText(charName); + } + return data.charName === charName; + }); +} + +export function verifyIfEmbedInDB( + db: Settings, + message: Djs.Message, + userId: string, + userName?: string +): { isInDb: boolean; coord?: PersonnageIds } { + const charData = db.get(message.guild!.id, `user.${userId}`); + if (!charData) return { isInDb: false }; + const charName = charData.find((char) => { + if (userName && char.charName) + return char.charName.standardize() === userName.standardize(); + return char.charName == null && userName == null; + }); + if (!charName) return { isInDb: false }; + const ids: PersonnageIds = { + channelId: charName.messageId[1], + messageId: charName.messageId[0], + }; + return { + isInDb: message.channel.id === ids.channelId && message.id === ids.messageId, + coord: ids, + }; +} + +/** + * Get the userName and the char from the embed between an interaction (button or modal), throw error if not found + */ +export async function getUserNameAndChar( + interaction: Djs.ButtonInteraction | Djs.ModalSubmitInteraction, + ul: Translation, + first?: boolean +) { + let userEmbed = getEmbeds(ul, interaction?.message ?? undefined, "user"); + if (first) { + const firstEmbed = ensureEmbed(interaction?.message ?? undefined); + if (firstEmbed) userEmbed = new Djs.EmbedBuilder(firstEmbed.toJSON()); + } + if (!userEmbed) throw new Error(ul("error.noEmbed")); + const userID = userEmbed + .toJSON() + .fields?.find((field) => findln(field.name) === "common.user") + ?.value.replace("<@", "") + .replace(">", ""); + if (!userID) throw new Error(ul("error.user")); + if ( + !interaction.channel || + (!(interaction.channel instanceof Djs.ThreadChannel) && + !(interaction.channel instanceof Djs.TextChannel)) + ) + throw new Error(ul("error.noThread")); + let userName = userEmbed + .toJSON() + .fields?.find((field) => findln(field.name) === "common.character")?.value; + if (userName === ul("common.noSet")) userName = undefined; + return { userID, userName, thread: interaction.channel }; +} diff --git a/packages/bot/src/database/index.ts b/packages/bot/src/database/index.ts new file mode 100644 index 00000000..a79d5194 --- /dev/null +++ b/packages/bot/src/database/index.ts @@ -0,0 +1,4 @@ +export * from "./delete_user"; +export * from "./get_template"; +export * from "./register_user"; +export * from "./get_user"; diff --git a/packages/bot/src/database/register_user.ts b/packages/bot/src/database/register_user.ts new file mode 100644 index 00000000..ec2a21a6 --- /dev/null +++ b/packages/bot/src/database/register_user.ts @@ -0,0 +1,82 @@ +import { ln } from "@dicelette/localization"; +import type { PersonnageIds, UserRegistration } from "@dicelette/types"; +import type { Settings } from "@dicelette/types"; +import type * as Djs from "discord.js"; +import { searchUserChannel } from "utils"; + +/** + * Register the managerId in the database + */ +export function setDefaultManagerId( + guildData: Settings, + interaction: Djs.BaseInteraction, + channel?: string +) { + if (!channel || !interaction.guild) return; + guildData.set(interaction.guild.id, channel, "managerId"); +} + +/** + * Register an user in the database + * @returns + */ +export async function registerUser( + userData: UserRegistration, + interaction: Djs.BaseInteraction, + enmap: Settings, + deleteMsg: boolean | undefined = true, + errorOnDuplicate: boolean | undefined = false +) { + const { userID, charName, msgId, isPrivate, damage } = userData; + const ids: PersonnageIds = { channelId: msgId[1], messageId: msgId[0] }; + if (!interaction.guild) return; + const guildData = enmap.get(interaction.guild.id); + if (!guildData) return; + if (!guildData.user) guildData.user = {}; + + const user = enmap.get(interaction.guild.id, `user.${userID}`); + const newChar = { + charName, + messageId: msgId, + damageName: damage, + isPrivate, + }; + //biome-ignore lint/performance/noDelete: We need to delete the key if it's not needed (because we are registering in the DB and undefined can lead to a bug) + if (!charName) delete newChar.charName; + //biome-ignore lint/performance/noDelete: We need to delete the key if it's not needed (because we are registering in the DB and undefined can lead to a bug) + if (!damage) delete newChar.damageName; + if (user) { + const char = user.find((char) => { + return char.charName?.subText(charName, true); + }); + const charIndex = user.findIndex((char) => { + return char.charName?.subText(charName, true); + }); + if (char) { + if (errorOnDuplicate) throw new Error("DUPLICATE"); + //delete old message + if (deleteMsg) { + try { + const threadOfChar = await searchUserChannel( + enmap, + interaction, + ln(interaction.locale), + ids.channelId + ); + if (threadOfChar) { + const oldMessage = await threadOfChar.messages.fetch(char.messageId[1]); + if (oldMessage) await oldMessage.delete(); + } + } catch (error) { + //skip unknown message + } + } + //overwrite the message id + char.messageId = msgId; + if (damage) char.damageName = damage; + enmap.set(interaction.guild.id, char, `user.${userID}.${charIndex}`); + } else enmap.set(interaction.guild.id, [...user, newChar], `user.${userID}`); + return; + } + enmap.set(interaction.guild.id, [newChar], `user.${userID}`); +} diff --git a/packages/bot/src/events/index.ts b/packages/bot/src/events/index.ts new file mode 100644 index 00000000..8929d8fc --- /dev/null +++ b/packages/bot/src/events/index.ts @@ -0,0 +1,8 @@ +import onInteraction from "./on_interaction"; +import onJoin from "./on_join"; +import onMessageSend from "./on_message_send"; +import ready from "./ready"; +export * from "./on_delete"; +export * from "./on_message_reaction"; +export { ready, onInteraction, onJoin, onMessageSend }; +export { default as onError } from "./on_error"; diff --git a/packages/bot/src/events/on_delete.ts b/packages/bot/src/events/on_delete.ts new file mode 100644 index 00000000..1f75b525 --- /dev/null +++ b/packages/bot/src/events/on_delete.ts @@ -0,0 +1,81 @@ +import type { PersonnageIds } from "@dicelette/types"; +import { logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import { deleteIfChannelOrThread } from "database"; +import { sendLogs } from "messages"; + +export const onDeleteChannel = (client: EClient): void => { + client.on("channelDelete", async (channel) => { + try { + if (channel.isDMBased()) return; + const guildID = channel.guild.id; + const db = client.settings; + deleteIfChannelOrThread(db, guildID, channel); + } catch (error) { + logger.error(error); + if (channel.isDMBased()) return; + await sendLogs((error as Error).message, channel.guild, client.settings); + } + }); +}; +export const onKick = (client: EClient): void => { + client.on("guildDelete", async (guild) => { + //delete guild from database + try { + client.settings.delete(guild.id); + } catch (error) { + logger.error(error); + } + }); +}; + +export const onDeleteThread = (client: EClient): void => { + client.on("threadDelete", async (thread) => { + try { + //search channelID in database and delete it + const guildID = thread.guild.id; + const db = client.settings; + //verify if the user message was in the thread + deleteIfChannelOrThread(db, guildID, thread); + } catch (error) { + logger.error(error); + if (thread.isDMBased()) return; + await sendLogs((error as Error).message, thread.guild, client.settings); + } + }); +}; +export const onDeleteMessage = (client: EClient): void => { + client.on("messageDelete", async (message) => { + try { + if (!message.guild) return; + const messageId = message.id; + //search channelID in database and delete it + const guildID = message.guild.id; + const channel = message.channel; + if (channel.isDMBased()) return; + if (client.settings.get(guildID, "templateID.messageId") === messageId) + client.settings.delete(guildID, "templateID"); + + const dbUser = client.settings.get(guildID, "user"); + if (dbUser && Object.keys(dbUser).length > 0) { + for (const [user, values] of Object.entries(dbUser)) { + for (const [index, value] of values.entries()) { + const persoId: PersonnageIds = { + messageId: value.messageId[0], + channelId: value.messageId[1], + }; + if (persoId.messageId === messageId && persoId.channelId === channel.id) { + logger.silly(`Deleted character ${value.charName} for user ${user}`); + values.splice(index, 1); + } + } + if (values.length === 0) delete dbUser[user]; + } + } + client.settings.set(guildID, dbUser, "user"); + } catch (error) { + if (!message.guild) return; + await sendLogs((error as Error).message, message.guild, client.settings); + } + }); +}; diff --git a/src/events/on_error.ts b/packages/bot/src/events/on_error.ts similarity index 71% rename from src/events/on_error.ts rename to packages/bot/src/events/on_error.ts index b80c9eb3..a1f2164d 100644 --- a/src/events/on_error.ts +++ b/packages/bot/src/events/on_error.ts @@ -1,10 +1,10 @@ -import { logger } from "@logger"; -import type { EClient } from "@main"; +import { logger } from "@dicelette/utils"; +import type { EClient } from "client"; import dotenv from "dotenv"; dotenv.config({ path: ".env" }); -export const botError = (client: EClient): void => { +export default (client: EClient): void => { client.on("error", async (error) => { logger.fatal(error); if (!process.env.OWNER_ID) return; diff --git a/src/events/interaction.ts b/packages/bot/src/events/on_interaction.ts similarity index 59% rename from src/events/interaction.ts rename to packages/bot/src/events/on_interaction.ts index 7193e7e4..a518ff14 100644 --- a/src/events/interaction.ts +++ b/packages/bot/src/events/on_interaction.ts @@ -1,27 +1,20 @@ -import { autCompleteCmd, commandsList } from "@commands"; -import { commandMenu, desktopLink, mobileLink } from "@commands/context-menu"; -import { resetButton } from "@commands/gimmick/edit"; import type { StatisticalTemplate } from "@dicelette/core"; -import { executeAddDiceButton, storeDamageDice } from "@interactions/add/dice"; -import { initiateAvatarEdit, validateAvatarEdit } from "@interactions/edit/avatar"; -import { initiateDiceEdit, validateDiceEdit } from "@interactions/edit/dice"; -import { initiateRenaming, validateRename } from "@interactions/edit/rename"; -import { editStats, triggerEditStats } from "@interactions/edit/stats"; -import { initiateMove, validateMove } from "@interactions/edit/user"; -import type { Settings, Translation } from "@interfaces/discord"; -import { lError, ln } from "@localization"; -import type { EClient } from "@main"; +import { lError, ln } from "@dicelette/localization"; +import type { Settings, Translation } from "@dicelette/types"; +import type { EClient } from "client"; import { - continuePage, - pageNumber, - recordFirstPage, - startRegisterUser, -} from "@register/start"; -import { validateUserButton } from "@register/validate"; -import { embedError, reply } from "@utils"; -import { getTemplate, getTemplateWithDB } from "@utils/db"; -import { ensureEmbed } from "@utils/parse"; + autCompleteCmd, + commandMenu, + commandsList, + desktopLink, + mobileLink, +} from "commands"; +import { resetButton } from "commands"; +import { getTemplate, getTemplateWithDB } from "database"; import * as Djs from "discord.js"; +import * as features from "features"; +import { embedError, reply } from "messages"; +import { cancel } from "utils"; export default (client: EClient): void => { client.on("interactionCreate", async (interaction: Djs.BaseInteraction) => { @@ -48,7 +41,7 @@ export default (client: EClient): void => { if (!command) return; await command.autocomplete(autocompleteInteraction, client); } else if (interaction.isButton()) { - let template = await getTemplate(interaction); + let template = await getTemplate(interaction.message, client.settings); template = template ? template : await getTemplateWithDB(interaction, client.settings); @@ -80,7 +73,9 @@ export default (client: EClient): void => { if (client.settings.has(interaction.guild.id)) { const db = client.settings.get(interaction.guild.id, "logs"); if (!db) return; - const logs = await interaction.guild.channels.fetch(db); + const logs = (await interaction.guild.channels.fetch( + db + )) as Djs.GuildBasedChannel; if (logs instanceof Djs.TextChannel) { await logs.send(`\`\`\`\n${(e as Error).message}\n\`\`\``); } @@ -104,17 +99,21 @@ async function modalSubmit( ) { const db = client.settings; if (interaction.customId.includes("damageDice")) - await storeDamageDice(interaction, ul, interactionUser, db); - else if (interaction.customId.includes("page")) await pageNumber(interaction, ul, db); - else if (interaction.customId === "editStats") await editStats(interaction, ul, db); - else if (interaction.customId === "firstPage") await recordFirstPage(interaction, db); + await features.storeDamageDice(interaction, ul, interactionUser, db); + else if (interaction.customId.includes("page")) + await features.pageNumber(interaction, ul, db); + else if (interaction.customId === "editStats") + await features.editStats(interaction, ul, db); + else if (interaction.customId === "firstPage") + await features.recordFirstPage(interaction, db); else if (interaction.customId === "editDice") - await validateDiceEdit(interaction, ul, db); + await features.validateDiceEdit(interaction, ul, db); else if (interaction.customId === "editAvatar") - await validateAvatarEdit(interaction, ul); + await features.validateAvatarEdit(interaction, ul); else if (interaction.customId === "rename") - await validateRename(interaction, ul, client); - else if (interaction.customId === "move") await validateMove(interaction, ul, client); + await features.validateRename(interaction, ul, client); + else if (interaction.customId === "move") + await features.validateMove(interaction, ul, client); } /** @@ -133,7 +132,7 @@ async function buttonSubmit( db: Settings ) { if (interaction.customId === "register") - await startRegisterUser( + await features.startRegisterUser( interaction, template, interactionUser, @@ -141,17 +140,17 @@ async function buttonSubmit( db.has(interaction.guild!.id, "privateChannel") ); else if (interaction.customId === "continue") - await continuePage(interaction, template, ul, interactionUser); + await features.continuePage(interaction, template, ul, interactionUser); else if (interaction.customId.includes("add_dice")) - await executeAddDiceButton(interaction, interactionUser, db); + await features.executeAddDiceButton(interaction, interactionUser, db); else if (interaction.customId === "edit_stats") - await triggerEditStats(interaction, ul, interactionUser, db); + await features.triggerEditStats(interaction, ul, interactionUser, db); else if (interaction.customId === "validate") - await validateUserButton(interaction, interactionUser, template, ul, db); + await features.validateUserButton(interaction, interactionUser, template, ul, db); else if (interaction.customId === "cancel") await cancel(interaction, ul, interactionUser); else if (interaction.customId === "edit_dice") - await initiateDiceEdit(interaction, ul, interactionUser, db); + await features.initiateDiceEdit(interaction, ul, interactionUser, db); else if (interaction.customId === "avatar") { await resetButton(interaction.message, ul); await interaction.reply({ content: ul("refresh"), ephemeral: true }); @@ -161,7 +160,7 @@ async function buttonSubmit( const message = await interaction.message.fetch(); if (isMobile) await mobileLink(interaction, ul); else await desktopLink(interaction, ul); - message.edit({ components: [] }); + await message.edit({ components: [] }); } } @@ -174,34 +173,10 @@ async function selectSubmit( if (interaction.customId === "edit_select") { const value = interaction.values[0]; if (value === "avatar") - await initiateAvatarEdit(interaction, ul, interactionUser, db); + await features.initiateAvatarEdit(interaction, ul, interactionUser, db); else if (value === "name") - await initiateRenaming(interaction, ul, interactionUser, db); - else if (value === "user") await initiateMove(interaction, ul, interactionUser, db); + await features.initiateRenaming(interaction, ul, interactionUser, db); + else if (value === "user") + await features.initiateMove(interaction, ul, interactionUser, db); } } - -/** - * Interaction when the cancel button is pressed - * Also prevent to cancel by user not authorized - * @param interaction {Djs.ButtonInteraction} - * @param ul {Translation} - * @param interactionUser {User} - */ -async function cancel( - interaction: Djs.ButtonInteraction, - ul: Translation, - interactionUser: Djs.User -) { - const embed = ensureEmbed(interaction.message); - const user = - embed.fields - .find((field) => field.name === ul("common.user")) - ?.value.replace("<@", "") - .replace(">", "") === interactionUser.id; - const isModerator = interaction.guild?.members.cache - .get(interactionUser.id) - ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); - if (user || isModerator) await interaction.message.delete(); - else await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); -} diff --git a/src/events/join.ts b/packages/bot/src/events/on_join.ts similarity index 73% rename from src/events/join.ts rename to packages/bot/src/events/on_join.ts index fb57bfe9..fd94956e 100644 --- a/src/events/join.ts +++ b/packages/bot/src/events/on_join.ts @@ -1,7 +1,6 @@ -import { commandsList } from "@commands"; -import { contextMenus } from "@commands/context-menu"; -import { logger } from "@logger"; -import type { EClient } from "@main"; +import { logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import { commandsList, contextMenus } from "commands"; export default (client: EClient): void => { client.on("guildCreate", async (guild) => { diff --git a/src/events/MessageReactionAdd.ts b/packages/bot/src/events/on_message_reaction.ts similarity index 97% rename from src/events/MessageReactionAdd.ts rename to packages/bot/src/events/on_message_reaction.ts index 9b33c186..835f488d 100644 --- a/src/events/MessageReactionAdd.ts +++ b/packages/bot/src/events/on_message_reaction.ts @@ -1,6 +1,6 @@ -import { ln } from "@localization/index"; -import { logger } from "@logger"; -import type { EClient } from "@main"; +import { ln } from "@dicelette/localization"; +import { logger } from "@dicelette/utils"; +import type { EClient } from "client"; import * as Djs from "discord.js"; export const onReactionAdd = (client: EClient): void => { diff --git a/packages/bot/src/events/on_message_send.ts b/packages/bot/src/events/on_message_send.ts new file mode 100644 index 00000000..d411a279 --- /dev/null +++ b/packages/bot/src/events/on_message_send.ts @@ -0,0 +1,102 @@ +import { lError, ln } from "@dicelette/localization"; +import { ResultAsText, isRolling, rollContent } from "@dicelette/parse_result"; +import { logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import * as Djs from "discord.js"; +import { deleteAfter, findMessageBefore, threadToSend } from "messages"; + +export default (client: EClient): void => { + client.on("messageCreate", async (message) => { + try { + if (message.author.bot) return; + if (message.channel.type === Djs.ChannelType.DM) return; + if (!message.guild) return; + const content = message.content; + //detect roll between bracket + const isRoll = isRolling(content); + if (!isRoll) return; + const { result, detectRoll } = isRoll; + const deleteInput = !detectRoll; + + //is a valid roll as we are in the function so we can work as always + const userLang = + client.settings.get(message.guild.id, "lang") ?? + message.guild.preferredLocale ?? + Djs.Locale.EnglishUS; + const ul = ln(userLang); + const channel = message.channel; + if (!result) return; + const resultAsText = new ResultAsText(result, { lang: userLang }); + const parser = resultAsText.parser; + if (!parser) return; + if ( + channel.name.startsWith("🎲") || + client.settings.get(message.guild.id, "disableThread") === true || + client.settings.get(message.guild.id, "rollChannel") === channel.id + ) { + await message.reply({ content: parser, allowedMentions: { repliedUser: true } }); + return; + } + let linkToOriginal = ""; + if (deleteInput) { + if (client.settings.get(message.guild.id, "context")) { + const messageBefore = await findMessageBefore(channel, message, client); + if (messageBefore) + linkToOriginal = resultAsText.createUrl({ + guildId: message.guildId ?? "", + channelId: channel.id, + messageId: messageBefore.id, + }); + } + } else { + linkToOriginal = resultAsText.createUrl({ + guildId: message.guildId ?? "", + channelId: channel.id, + messageId: message.id, + }); + } + const thread = await threadToSend(client.settings, channel, ul); + const msgToEdit = await thread.send("_ _"); + const msg = rollContent( + result, + parser, + linkToOriginal, + message.author.id, + client.settings.get(message.guild.id, "timestamp") + ); + await msgToEdit.edit(msg); + const idMessage = client.settings.get(message.guild.id, "linkToLogs") + ? resultAsText.createUrl(undefined, msgToEdit.url) + : ""; + const reply = deleteInput + ? await channel.send({ + content: rollContent(result, parser, idMessage, message.author.id), + }) + : await message.reply({ + content: rollContent(result, parser, idMessage), + allowedMentions: { repliedUser: true }, + }); + const timer = client.settings.get(message.guild.id, "deleteAfter") ?? 180000; + await deleteAfter(reply, timer); + if (deleteInput) await message.delete(); + return; + } catch (e) { + logger.error(e); + if (!message.guild) return; + const userLang = + client.settings.get(message.guild.id, "lang") ?? + message.guild.preferredLocale ?? + Djs.Locale.EnglishUS; + const msgError = lError(e as Error, undefined, userLang); + if (msgError.length === 0) return; + await message.channel.send({ content: msgError }); + const logsId = client.settings.get(message.guild.id, "logs"); + if (logsId) { + const logs = await message.guild.channels.fetch(logsId); + if (logs instanceof Djs.TextChannel) { + await logs.send(`\`\`\`\n${(e as Error).message}\n\`\`\``); + } + } + } + }); +}; diff --git a/src/events/ready.ts b/packages/bot/src/events/ready.ts similarity index 78% rename from src/events/ready.ts rename to packages/bot/src/events/ready.ts index 2807d090..ca7b33ae 100644 --- a/src/events/ready.ts +++ b/packages/bot/src/events/ready.ts @@ -1,15 +1,16 @@ -import process from "node:process"; -import { commandsList } from "@commands"; -import { contextMenus } from "@commands/context-menu"; -import type { Settings } from "@interfaces/discord"; -import { logger } from "@logger"; -import { type EClient, VERSION } from "@main"; -import { ActivityType, type Guild, REST, Routes } from "discord.js"; -import dotenv from "dotenv"; +// noinspection ES6MissingAwait +import type { Settings } from "@dicelette/types"; +import { logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import { commandsList, contextMenus } from "commands"; +import * as Djs from "discord.js"; +import type { Guild } from "discord.js"; +import dotenv from "dotenv"; +import { VERSION } from "../../index.js"; dotenv.config({ path: ".env" }); -const rest = new REST().setToken(process.env.DISCORD_TOKEN ?? "0"); +const rest = new Djs.REST().setToken(process.env.DISCORD_TOKEN ?? "0"); export default (client: EClient): void => { client.on("ready", async () => { @@ -17,7 +18,7 @@ export default (client: EClient): void => { logger.trace(`${client.user.username} is online; v.${VERSION}`); let serializedCommands = commandsList.map((command) => command.data.toJSON()); - client.user.setActivity("Roll Dices 🎲 !", { type: ActivityType.Competing }); + client.user.setActivity("Roll Dices 🎲 !", { type: Djs.ActivityType.Competing }); serializedCommands = serializedCommands.concat( //@ts-ignore contextMenus.map((cmd) => cmd.toJSON()) @@ -26,7 +27,7 @@ export default (client: EClient): void => { logger.trace(`Registering commands for \`${guild.name}\``); const cmds = await guild.client.application.commands.fetch({ guildId: guild.id }); //filter the list of the commands that are deleted - // biome-ignore lint/complexity/noForEach: forEach is fine here noinspection ES6MissingAwait + // biome-ignore lint/complexity/noForEach: { if (serializedCommands.find((c) => c.name === command.name)) return; try { @@ -36,9 +37,12 @@ export default (client: EClient): void => { } }); - await rest.put(Routes.applicationGuildCommands(process.env.CLIENT_ID, guild.id), { - body: serializedCommands, - }); + await rest.put( + Djs.Routes.applicationGuildCommands(process.env.CLIENT_ID, guild.id), + { + body: serializedCommands, + } + ); convertDatabaseUser(client.settings, guild); } diff --git a/packages/bot/src/features/avatar/index.ts b/packages/bot/src/features/avatar/index.ts new file mode 100644 index 00000000..be7d9760 --- /dev/null +++ b/packages/bot/src/features/avatar/index.ts @@ -0,0 +1,2 @@ +export * from "./modal"; +export * from "./validation"; diff --git a/packages/bot/src/features/avatar/modal.ts b/packages/bot/src/features/avatar/modal.ts new file mode 100644 index 00000000..45d8e001 --- /dev/null +++ b/packages/bot/src/features/avatar/modal.ts @@ -0,0 +1,37 @@ +import type { Settings, Translation } from "@dicelette/types"; +import * as Djs from "discord.js"; +import { getEmbeds } from "messages"; +import { allowEdit } from "utils"; + +export async function initiateAvatarEdit( + interaction: Djs.StringSelectMenuInteraction, + ul: Translation, + interactionUser: Djs.User, + db: Settings +) { + if (await allowEdit(interaction, db, interactionUser)) + await showAvatarEdit(interaction, ul); +} + +export async function showAvatarEdit( + interaction: Djs.StringSelectMenuInteraction, + ul: Translation +) { + const embed = getEmbeds(ul, interaction.message, "user"); + if (!embed) throw new Error(ul("error.noEmbed")); + const thumbnail = embed.toJSON().thumbnail?.url ?? interaction.user.displayAvatarURL(); + const modal = new Djs.ModalBuilder() + .setCustomId("editAvatar") + .setTitle(ul("button.avatar.description")); + const input = + new Djs.ActionRowBuilder().addComponents( + new Djs.TextInputBuilder() + .setCustomId("avatar") + .setLabel(ul("modals.avatar.name")) + .setRequired(true) + .setStyle(Djs.TextInputStyle.Short) + .setValue(thumbnail) + ); + modal.addComponents(input); + await interaction.showModal(modal); +} diff --git a/packages/bot/src/features/avatar/validation.ts b/packages/bot/src/features/avatar/validation.ts new file mode 100644 index 00000000..fc50eaa2 --- /dev/null +++ b/packages/bot/src/features/avatar/validation.ts @@ -0,0 +1,38 @@ +import { findln } from "@dicelette/localization"; +import type { Translation } from "@dicelette/types"; +import { verifyAvatarUrl } from "@dicelette/utils"; +import type * as Djs from "discord.js"; +import { embedError, getEmbeds, getEmbedsList, reply } from "messages"; + +export async function validateAvatarEdit( + interaction: Djs.ModalSubmitInteraction, + ul: Translation +) { + if (!interaction.message) return; + const avatar = interaction.fields.getTextInputValue("avatar"); + if (avatar.match(/(cdn|media)\.discordapp\.net/gi)) + return await reply(interaction, { + embeds: [embedError(ul("error.avatar.discord"), ul)], + }); + if (!verifyAvatarUrl(avatar)) + return await reply(interaction, { embeds: [embedError(ul("error.avatar.url"), ul)] }); + + const embed = getEmbeds(ul, interaction.message, "user"); + if (!embed) throw new Error(ul("error.noEmbed")); + embed.setThumbnail(avatar); + const embedsList = getEmbedsList(ul, { which: "user", embed }, interaction.message); + await interaction.message.edit({ embeds: embedsList.list }); + const user = embed + .toJSON() + .fields?.find((field) => findln(field.name) === "common.user")?.value; + const charName = embed + .toJSON() + .fields?.find((field) => findln(field.name) === "common.character")?.value; + const nameMention = + !charName || findln(charName) === "common.noSet" ? user : `${user} (${charName})`; + const msgLink = interaction.message.url; + await reply(interaction, { + content: ul("edit_avatar.success", { name: nameMention, link: msgLink }), + ephemeral: true, + }); +} diff --git a/packages/bot/src/features/dice/buttons.ts b/packages/bot/src/features/dice/buttons.ts new file mode 100644 index 00000000..1f655c7e --- /dev/null +++ b/packages/bot/src/features/dice/buttons.ts @@ -0,0 +1,116 @@ +import { ln } from "@dicelette/localization"; +import type { Settings, Translation } from "@dicelette/types"; +import * as Djs from "discord.js"; +import { getEmbeds, parseEmbedFields } from "messages"; +import { allowEdit } from "utils"; + +/** + * Interaction to add a new skill dice + * @param interaction {Djs.ButtonInteraction} + * @param interactionUser {User} + * @param db + */ +export async function executeAddDiceButton( + interaction: Djs.ButtonInteraction, + interactionUser: Djs.User, + db: Settings +) { + const allow = await allowEdit(interaction, db, interactionUser); + if (allow) + await showDamageDiceModals( + interaction, + interaction.customId.includes("first"), + db.get(interaction.guild!.id, "lang") ?? interaction.locale + ); +} + +/** + * Modal to add a new skill dice + * @param interaction {Djs.ButtonInteraction} + * @param first {boolean} + * - true: It's the modal when the user is registered + * - false: It's the modal when the user is already registered and a new dice is added to edit the user + * @param lang + */ +export async function showDamageDiceModals( + interaction: Djs.ButtonInteraction, + first?: boolean, + lang: Djs.Locale = Djs.Locale.EnglishGB +) { + const ul = ln(lang); + const id = first ? "damageDice_first" : "damageDice"; + const modal = new Djs.ModalBuilder() + .setCustomId(id) + .setTitle(ul("register.embed.damage")); + const damageDice = + new Djs.ActionRowBuilder().addComponents( + new Djs.TextInputBuilder() + .setCustomId("damageName") + .setLabel(ul("modals.dice.name")) + .setPlaceholder(ul("modals.dice.placeholder")) + .setRequired(true) + .setValue("") + .setStyle(Djs.TextInputStyle.Short) + ); + const diceValue = + new Djs.ActionRowBuilder().addComponents( + new Djs.TextInputBuilder() + .setCustomId("damageValue") + .setLabel(ul("modals.dice.value")) + .setPlaceholder("1d5") + .setRequired(true) + .setValue("") + .setStyle(Djs.TextInputStyle.Short) + ); + modal.addComponents(damageDice); + modal.addComponents(diceValue); + await interaction.showModal(modal); +} + +/** + * Start the showEditDice when the button is interacted + * It will also verify if the user can edit their dice + * @param interaction {Djs.ButtonInteraction} + * @param ul {Translation} + * @param interactionUser {Djs.User} + * @param db {Settings} + */ +export async function initiateDiceEdit( + interaction: Djs.ButtonInteraction, + ul: Translation, + interactionUser: Djs.User, + db: Settings +) { + if (await allowEdit(interaction, db, interactionUser)) + await showEditDice(interaction, ul); +} + +/** + * Show the modal to **edit** the registered dice + * Will parse registered dice and show them in the modal as `- Skill : Dice` + * @param interaction {Djs.ButtonInteraction} + * @param ul {Translation} + */ +export async function showEditDice(interaction: Djs.ButtonInteraction, ul: Translation) { + const diceEmbed = getEmbeds(ul, interaction.message, "damage"); + if (!diceEmbed) throw new Error(ul("error.invalidDice.embeds")); + const diceFields = parseEmbedFields(diceEmbed.toJSON() as Djs.Embed); + let dices = ""; + for (const [skill, dice] of Object.entries(diceFields)) { + dices += `- ${skill}${ul("common.space")}: ${dice}\n`; + } + const modal = new Djs.ModalBuilder() + .setCustomId("editDice") + .setTitle(ul("common.dice").capitalize()); + const input = + new Djs.ActionRowBuilder().addComponents( + new Djs.TextInputBuilder() + .setCustomId("allDice") + .setLabel(ul("modals.edit.dice")) + .setRequired(true) + .setStyle(Djs.TextInputStyle.Paragraph) + .setValue(dices) + ); + modal.addComponents(input); + await interaction.showModal(modal); +} diff --git a/packages/bot/src/features/dice/index.ts b/packages/bot/src/features/dice/index.ts new file mode 100644 index 00000000..162570a8 --- /dev/null +++ b/packages/bot/src/features/dice/index.ts @@ -0,0 +1,3 @@ +export * from "./buttons"; +export * from "./validation"; +export * from "./register"; diff --git a/src/interactions/add/dice.ts b/packages/bot/src/features/dice/register.ts similarity index 69% rename from src/interactions/add/dice.ts rename to packages/bot/src/features/dice/register.ts index d9449904..721f15da 100644 --- a/src/interactions/add/dice.ts +++ b/packages/bot/src/features/dice/register.ts @@ -1,75 +1,24 @@ import { evalStatsDice } from "@dicelette/core"; -import { allowEdit, createDiceEmbed, getUserNameAndChar } from "@interactions"; -import type { UserMessageId } from "@interfaces/database"; -import type { Settings, Translation } from "@interfaces/discord"; -import { findln, ln } from "@localization"; -import { NoEmbed, addAutoRole, embedError, reply, sendLogs } from "@utils"; -import { editUserButtons, registerDmgButton } from "@utils/buttons"; -import { getTemplateWithDB, getUserByEmbed, registerUser } from "@utils/db"; -import { ensureEmbed, getEmbeds } from "@utils/parse"; +import { findln, ln } from "@dicelette/localization"; +import type { UserMessageId } from "@dicelette/types"; +import type { Settings, Translation } from "@dicelette/types"; +import { NoEmbed } from "@dicelette/utils"; +import { + getTemplateWithDB, + getUserByEmbed, + getUserNameAndChar, + registerUser, +} from "database"; import * as Djs from "discord.js"; -/** - * Interaction to add a new skill dice - * @param interaction {Djs.ButtonInteraction} - * @param interactionUser {User} - * @param db - */ -export async function executeAddDiceButton( - interaction: Djs.ButtonInteraction, - interactionUser: Djs.User, - db: Settings -) { - const allow = await allowEdit(interaction, db, interactionUser); - if (allow) - showDamageDiceModals( - interaction, - interaction.customId.includes("first"), - db.get(interaction.guild!.id, "lang") ?? interaction.locale - ); -} - -/** - * Modal to add a new skill dice - * @param interaction {Djs.ButtonInteraction} - * @param first {boolean} - * - true: It's the modal when the user is registered - * - false: It's the modal when the user is already registered and a new dice is added to edit the user - * @param lang - */ -export async function showDamageDiceModals( - interaction: Djs.ButtonInteraction, - first?: boolean, - lang: Djs.Locale = Djs.Locale.EnglishGB -) { - const ul = ln(lang); - const id = first ? "damageDice_first" : "damageDice"; - const modal = new Djs.ModalBuilder() - .setCustomId(id) - .setTitle(ul("register.embed.damage")); - const damageDice = - new Djs.ActionRowBuilder().addComponents( - new Djs.TextInputBuilder() - .setCustomId("damageName") - .setLabel(ul("modals.dice.name")) - .setPlaceholder(ul("modals.dice.placeholder")) - .setRequired(true) - .setValue("") - .setStyle(Djs.TextInputStyle.Short) - ); - const diceValue = - new Djs.ActionRowBuilder().addComponents( - new Djs.TextInputBuilder() - .setCustomId("damageValue") - .setLabel(ul("modals.dice.value")) - .setPlaceholder("1d5") - .setRequired(true) - .setValue("") - .setStyle(Djs.TextInputStyle.Short) - ); - modal.addComponents(damageDice); - modal.addComponents(diceValue); - await interaction.showModal(modal); -} +import { + createDiceEmbed, + embedError, + ensureEmbed, + getEmbeds, + reply, + sendLogs, +} from "messages"; +import { addAutoRole, editUserButtons } from "utils"; /** * Interaction to submit the new skill dice @@ -103,6 +52,31 @@ export async function storeDamageDice( await registerDamageDice(interaction, db, interaction.customId.includes("first")); else await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); } + +/** + * Button when registering the user, adding the "add dice" button + * @param ul {Translation} + */ +export function registerDmgButton(ul: Translation) { + const validateButton = new Djs.ButtonBuilder() + .setCustomId("validate") + .setLabel(ul("button.validate")) + .setStyle(Djs.ButtonStyle.Success); + const cancelButton = new Djs.ButtonBuilder() + .setCustomId("cancel") + .setLabel(ul("button.cancel")) + .setStyle(Djs.ButtonStyle.Danger); + const registerDmgButton = new Djs.ButtonBuilder() + .setCustomId("add_dice_first") + .setLabel(ul("button.dice")) + .setStyle(Djs.ButtonStyle.Primary); + return new Djs.ActionRowBuilder().addComponents([ + registerDmgButton, + validateButton, + cancelButton, + ]); +} + /** * Register the new skill dice in the embed and database * @param interaction {Djs.ModalSubmitInteraction} @@ -199,7 +173,7 @@ export async function registerDamageDice( damage: damageName ? Object.keys(damageName) : undefined, msgId: [interaction.message.id, interaction.message.channel.id], }; - registerUser(userRegister, interaction, db, false); + await registerUser(userRegister, interaction, db, false); await interaction?.message?.edit({ embeds: allEmbeds, components: [components] }); await reply(interaction, { content: ul("modals.added.dice"), ephemeral: true }); await sendLogs( diff --git a/src/interactions/edit/dice.ts b/packages/bot/src/features/dice/validation.ts similarity index 69% rename from src/interactions/edit/dice.ts rename to packages/bot/src/features/dice/validation.ts index 43081f9d..c58c6f20 100644 --- a/src/interactions/edit/dice.ts +++ b/packages/bot/src/features/dice/validation.ts @@ -1,46 +1,19 @@ import { evalStatsDice, roll } from "@dicelette/core"; -import { allowEdit, createDiceEmbed, getUserNameAndChar } from "@interactions"; -import type { UserMessageId, UserRegistration } from "@interfaces/database"; -import type { Settings, Translation } from "@interfaces/discord"; -import { displayOldAndNewStats, parseStatsString, reply, sendLogs } from "@utils"; -import { editUserButtons } from "@utils/buttons"; -import { registerUser } from "@utils/db"; +import type { UserMessageId, UserRegistration } from "@dicelette/types"; +import type { Settings, Translation } from "@dicelette/types"; +import { getUserNameAndChar, registerUser } from "database"; +import * as Djs from "discord.js"; import { + createDiceEmbed, + displayOldAndNewStats, getEmbeds, getEmbedsList, parseEmbedFields, removeEmbedsFromList, -} from "@utils/parse"; -import * as Djs from "discord.js"; -/** - * Show the modal to **edit** the registered dice - * Will parse registered dice and show them in the modal as `- Skill : Dice` - * @param interaction {Djs.ButtonInteraction} - * @param ul {Translation} - */ -export async function showEditDice(interaction: Djs.ButtonInteraction, ul: Translation) { - const diceEmbed = getEmbeds(ul, interaction.message, "damage"); - if (!diceEmbed) throw new Error(ul("error.invalidDice.embeds")); - const diceFields = parseEmbedFields(diceEmbed.toJSON() as Djs.Embed); - let dices = ""; - for (const [skill, dice] of Object.entries(diceFields)) { - dices += `- ${skill}${ul("common.space")}: ${dice}\n`; - } - const modal = new Djs.ModalBuilder() - .setCustomId("editDice") - .setTitle(ul("common.dice").capitalize()); - const input = - new Djs.ActionRowBuilder().addComponents( - new Djs.TextInputBuilder() - .setCustomId("allDice") - .setLabel(ul("modals.edit.dice")) - .setRequired(true) - .setStyle(Djs.TextInputStyle.Paragraph) - .setValue(dices) - ); - modal.addComponents(input); - await interaction.showModal(modal); -} + reply, + sendLogs, +} from "messages"; +import { editUserButtons } from "utils"; /** * Validate the edit of the dice from the modals @@ -154,7 +127,7 @@ export async function validateDiceEdit( damage: undefined, msgId: messageID, }; - registerUser(userRegister, interaction, db, false); + await registerUser(userRegister, interaction, db, false); await sendLogs( ul("logs.dice.remove", { user: Djs.userMention(interaction.user.id), @@ -181,7 +154,7 @@ export async function validateDiceEdit( damage: skillDiceName, msgId: messageID, }; - registerUser(userRegister, interaction, db, false); + await registerUser(userRegister, interaction, db, false); const embedsList = getEmbedsList( ul, { which: "damage", embed: diceEmbed }, @@ -199,19 +172,18 @@ export async function validateDiceEdit( } /** - * Start the showEditDice when the button is interacted - * It will also verify if the user can edit their dice - * @param interaction {Djs.ButtonInteraction} - * @param ul {Translation} - * @param interactionUser {Djs.User} - * @param db {Settings} + * Parse the fields in stats, used to fix combinaison and get only them and not their result */ -export async function initiateDiceEdit( - interaction: Djs.ButtonInteraction, - ul: Translation, - interactionUser: Djs.User, - db: Settings -) { - if (await allowEdit(interaction, db, interactionUser)) - await showEditDice(interaction, ul); +function parseStatsString(statsEmbed: Djs.EmbedBuilder) { + const stats = parseEmbedFields(statsEmbed.toJSON() as Djs.Embed); + const parsedStats: { [name: string]: number } = {}; + for (const [name, value] of Object.entries(stats)) { + let number = Number.parseInt(value, 10); + if (Number.isNaN(number)) { + const combinaison = value.replace(/`(.*)` =/, "").trim(); + number = Number.parseInt(combinaison, 10); + } + parsedStats[name] = number; + } + return parsedStats; } diff --git a/packages/bot/src/features/index.ts b/packages/bot/src/features/index.ts new file mode 100644 index 00000000..0ae7e574 --- /dev/null +++ b/packages/bot/src/features/index.ts @@ -0,0 +1,7 @@ +export * from "./dice"; +export * from "./avatar"; +export * from "./dice"; +export * from "./move"; +export * from "./rename"; +export * from "./stats"; +export * from "./user"; diff --git a/packages/bot/src/features/move/index.ts b/packages/bot/src/features/move/index.ts new file mode 100644 index 00000000..be7d9760 --- /dev/null +++ b/packages/bot/src/features/move/index.ts @@ -0,0 +1,2 @@ +export * from "./modal"; +export * from "./validation"; diff --git a/packages/bot/src/features/move/modal.ts b/packages/bot/src/features/move/modal.ts new file mode 100644 index 00000000..2b84303b --- /dev/null +++ b/packages/bot/src/features/move/modal.ts @@ -0,0 +1,28 @@ +import type { Settings, Translation } from "@dicelette/types"; +import * as Djs from "discord.js"; +import { allowEdit } from "utils"; + +export async function initiateMove( + interaction: Djs.StringSelectMenuInteraction, + ul: Translation, + interactionUser: Djs.User, + db: Settings +) { + if (await allowEdit(interaction, db, interactionUser)) await showMove(interaction, ul); +} + +async function showMove(interaction: Djs.StringSelectMenuInteraction, ul: Translation) { + const modal = new Djs.ModalBuilder() + .setCustomId("move") + .setTitle(ul("button.edit.move")); + const input = + new Djs.ActionRowBuilder().addComponents( + new Djs.TextInputBuilder() + .setCustomId("user") + .setLabel(ul("common.user")) + .setRequired(true) + .setStyle(Djs.TextInputStyle.Short) + ); + modal.addComponents(input); + await interaction.showModal(modal); +} diff --git a/src/interactions/edit/user.ts b/packages/bot/src/features/move/validation.ts similarity index 61% rename from src/interactions/edit/user.ts rename to packages/bot/src/features/move/validation.ts index 451b6def..028785a0 100644 --- a/src/interactions/edit/user.ts +++ b/packages/bot/src/features/move/validation.ts @@ -1,38 +1,16 @@ -import { move, resetButton } from "@commands/gimmick/edit"; -import { allowEdit } from "@interactions"; -import type { PersonnageIds, UserMessageId } from "@interfaces/database"; -import { findln } from "@localization"; -import type { EClient } from "@main"; -import { embedError } from "@utils"; -import { getUserByEmbed } from "@utils/db"; -import { isUserNameOrId } from "@utils/find"; -import { getEmbeds } from "@utils/parse"; -import * as Djs from "discord.js"; -import type { DiscordChannel, Settings, Translation } from "@interfaces/discord"; -export async function initiateMove( - interaction: Djs.StringSelectMenuInteraction, - ul: Translation, - interactionUser: Djs.User, - db: Settings -) { - if (await allowEdit(interaction, db, interactionUser)) await showMove(interaction, ul); -} - -async function showMove(interaction: Djs.StringSelectMenuInteraction, ul: Translation) { - const modal = new Djs.ModalBuilder() - .setCustomId("move") - .setTitle(ul("button.edit.move")); - const input = - new Djs.ActionRowBuilder().addComponents( - new Djs.TextInputBuilder() - .setCustomId("user") - .setLabel(ul("common.user")) - .setRequired(true) - .setStyle(Djs.TextInputStyle.Short) - ); - modal.addComponents(input); - await interaction.showModal(modal); -} +import { findln } from "@dicelette/localization"; +import type { + DiscordChannel, + PersonnageIds, + Translation, + UserMessageId, +} from "@dicelette/types"; +import type { EClient } from "client"; +import { move, resetButton } from "commands"; +import { getUserByEmbed } from "database"; +import type * as Djs from "discord.js"; +import { embedError, getEmbeds } from "messages"; +import { isUserNameOrId } from "utils"; export async function validateMove( interaction: Djs.ModalSubmitInteraction, diff --git a/packages/bot/src/features/rename/index.ts b/packages/bot/src/features/rename/index.ts new file mode 100644 index 00000000..be7d9760 --- /dev/null +++ b/packages/bot/src/features/rename/index.ts @@ -0,0 +1,2 @@ +export * from "./modal"; +export * from "./validation"; diff --git a/packages/bot/src/features/rename/modal.ts b/packages/bot/src/features/rename/modal.ts new file mode 100644 index 00000000..8329e8fd --- /dev/null +++ b/packages/bot/src/features/rename/modal.ts @@ -0,0 +1,32 @@ +import type { Settings, Translation } from "@dicelette/types"; +import * as Djs from "discord.js"; +import { allowEdit } from "utils"; + +export async function initiateRenaming( + interaction: Djs.StringSelectMenuInteraction, + ul: Translation, + interactionUser: Djs.User, + db: Settings +) { + if (await allowEdit(interaction, db, interactionUser)) + await showRename(interaction, ul); +} + +export async function showRename( + interaction: Djs.StringSelectMenuInteraction, + ul: Translation +) { + const modal = new Djs.ModalBuilder() + .setCustomId("rename") + .setTitle(ul("button.edit.name")); + const input = + new Djs.ActionRowBuilder().addComponents( + new Djs.TextInputBuilder() + .setCustomId("newName") + .setLabel(ul("common.character")) + .setRequired(true) + .setStyle(Djs.TextInputStyle.Short) + ); + modal.addComponents(input); + await interaction.showModal(modal); +} diff --git a/src/interactions/edit/rename.ts b/packages/bot/src/features/rename/validation.ts similarity index 54% rename from src/interactions/edit/rename.ts rename to packages/bot/src/features/rename/validation.ts index e89e7204..f41fba83 100644 --- a/src/interactions/edit/rename.ts +++ b/packages/bot/src/features/rename/validation.ts @@ -1,40 +1,12 @@ -import { rename } from "@commands/gimmick/edit"; -import { allowEdit } from "@interactions"; -import type { PersonnageIds, UserMessageId } from "@interfaces/database"; -import { findln } from "@localization"; -import type { EClient } from "@main"; -import { getUserByEmbed } from "@utils/db"; -import { getEmbeds } from "@utils/parse"; -import * as Djs from "discord.js"; -import type { DiscordChannel, Settings, Translation } from "@interfaces/discord"; -export async function initiateRenaming( - interaction: Djs.StringSelectMenuInteraction, - ul: Translation, - interactionUser: Djs.User, - db: Settings -) { - if (await allowEdit(interaction, db, interactionUser)) - await showRename(interaction, ul); -} - -export async function showRename( - interaction: Djs.StringSelectMenuInteraction, - ul: Translation -) { - const modal = new Djs.ModalBuilder() - .setCustomId("rename") - .setTitle(ul("button.edit.name")); - const input = - new Djs.ActionRowBuilder().addComponents( - new Djs.TextInputBuilder() - .setCustomId("newName") - .setLabel(ul("common.character")) - .setRequired(true) - .setStyle(Djs.TextInputStyle.Short) - ); - modal.addComponents(input); - await interaction.showModal(modal); -} +import { findln } from "@dicelette/localization"; +import type { Translation } from "@dicelette/types"; +import type { PersonnageIds, UserMessageId } from "@dicelette/types"; +import type { DiscordChannel } from "@dicelette/types"; +import type { EClient } from "client"; +import { rename } from "commands"; +import { getUserByEmbed } from "database"; +import type * as Djs from "discord.js"; +import { getEmbeds } from "messages"; export async function validateRename( interaction: Djs.ModalSubmitInteraction, diff --git a/packages/bot/src/features/stats/buttons.ts b/packages/bot/src/features/stats/buttons.ts new file mode 100644 index 00000000..48055b3d --- /dev/null +++ b/packages/bot/src/features/stats/buttons.ts @@ -0,0 +1,136 @@ +import type { StatisticalTemplate } from "@dicelette/core"; +import { ln } from "@dicelette/localization"; +import type { Settings, Translation } from "@dicelette/types"; +import { isArrayEqual } from "@dicelette/utils"; +import * as Djs from "discord.js"; +import { registerDmgButton } from "features"; +import { getEmbeds, parseEmbedFields, reply } from "messages"; +import { allowEdit } from "utils"; + +/** + * Modal to display the statistics when adding a new user + * Will display the statistics that are not already set + * 5 statistics per page + */ +export async function showStatistiqueModal( + interaction: Djs.ButtonInteraction, + template: StatisticalTemplate, + stats?: string[], + page = 1 +) { + if (!template.statistics) return; + const ul = ln(interaction.locale as Djs.Locale); + const statsWithoutCombinaison = + Object.keys(template.statistics).filter((stat) => { + return !template.statistics?.[stat]?.combinaison; + }) ?? []; + const nbOfPages = + Math.ceil(statsWithoutCombinaison.length / 5) >= 1 + ? Math.ceil(statsWithoutCombinaison.length / 5) + : page; + const modal = new Djs.ModalBuilder() + .setCustomId(`page${page}`) + .setTitle(ul("modals.steps", { page, max: nbOfPages + 1 })); + let statToDisplay = statsWithoutCombinaison; + if (stats && stats.length > 0) { + statToDisplay = statToDisplay.filter((stat) => !stats.includes(stat.unidecode())); + if (statToDisplay.length === 0) { + //remove button + const button = registerDmgButton(ul); + await reply(interaction, { content: ul("modals.alreadySet"), ephemeral: true }); + await interaction.message.edit({ components: [button] }); + } + } + const statsToDisplay = statToDisplay.slice(0, 4); + const statisticsLowerCase = Object.fromEntries( + Object.entries(template.statistics).map(([key, value]) => [key.standardize(), value]) + ); + for (const stat of statsToDisplay) { + const cleanedName = stat.unidecode(); + const value = statisticsLowerCase[cleanedName]; + if (value.combinaison) continue; + let msg = ""; + if (value.min && value.max) + msg = ul("modals.enterValue.minAndMax", { min: value.min, max: value.max }); + else if (value.min) msg = ul("modals.enterValue.minOnly", { min: value.min }); + else if (value.max) msg = ul("modals.enterValue.maxOnly", { max: value.max }); + const input = + new Djs.ActionRowBuilder().addComponents( + new Djs.TextInputBuilder() + .setCustomId(cleanedName) + .setLabel(stat) + .setPlaceholder(msg) + .setRequired(true) + .setValue("") + .setStyle(Djs.TextInputStyle.Short) + ); + modal.addComponents(input); + } + await interaction.showModal(modal); +} + +/** + * The button that trigger the stats editor + */ +export async function triggerEditStats( + interaction: Djs.ButtonInteraction, + ul: Translation, + interactionUser: Djs.User, + db: Settings +) { + if (await allowEdit(interaction, db, interactionUser)) + await showEditorStats(interaction, ul, db); +} + +/** + * Show the stats editor + */ +export async function showEditorStats( + interaction: Djs.ButtonInteraction, + ul: Translation, + db: Settings +) { + const statistics = getEmbeds(ul, interaction.message, "stats"); + if (!statistics) throw new Error(ul("error.statNotFound")); + const stats = parseEmbedFields(statistics.toJSON() as Djs.Embed); + const originalGuildData = db.get(interaction.guild!.id, "templateID.statsName"); + const registeredStats = originalGuildData?.map((stat) => stat.unidecode()); + const userStats = Object.keys(stats).map((stat) => stat.unidecode()); + let statsStrings = ""; + for (const [name, value] of Object.entries(stats)) { + let stringValue = value; + if (!registeredStats?.includes(name.unidecode())) continue; //remove stats that are not registered + if (value.match(/=/)) { + const combinaison = value.split("=")?.[0].trim(); + if (combinaison) stringValue = combinaison; + } + statsStrings += `- ${name}${ul("common.space")}: ${stringValue}\n`; + } + if ( + !isArrayEqual(registeredStats, userStats) && + registeredStats && + registeredStats.length > userStats.length + ) { + //check which stats was added + const diff = registeredStats.filter((x) => !userStats.includes(x)); + for (const stat of diff) { + const realName = originalGuildData?.find((x) => x.unidecode() === stat.unidecode()); + statsStrings += `- ${realName?.capitalize()}${ul("common.space")}: 0\n`; + } + } + + const modal = new Djs.ModalBuilder() + .setCustomId("editStats") + .setTitle(ul("common.statistics").capitalize()); + const input = + new Djs.ActionRowBuilder().addComponents( + new Djs.TextInputBuilder() + .setCustomId("allStats") + .setLabel(ul("modals.edit.stats")) + .setRequired(true) + .setStyle(Djs.TextInputStyle.Paragraph) + .setValue(statsStrings) + ); + modal.addComponents(input); + await interaction.showModal(modal); +} diff --git a/packages/bot/src/features/stats/index.ts b/packages/bot/src/features/stats/index.ts new file mode 100644 index 00000000..6ebc7413 --- /dev/null +++ b/packages/bot/src/features/stats/index.ts @@ -0,0 +1,2 @@ +export * from "./buttons"; +export * from "./modals"; diff --git a/src/interactions/edit/stats.ts b/packages/bot/src/features/stats/modals.ts similarity index 61% rename from src/interactions/edit/stats.ts rename to packages/bot/src/features/stats/modals.ts index f9008727..53885aa6 100644 --- a/src/interactions/edit/stats.ts +++ b/packages/bot/src/features/stats/modals.ts @@ -1,16 +1,101 @@ -import { FormulaError, evalOneCombinaison } from "@dicelette/core"; -import { allowEdit, createStatsEmbed, getUserNameAndChar } from "@interactions"; -import type { Settings, Translation } from "@interfaces/discord"; -import { displayOldAndNewStats, isArrayEqual, reply, sendLogs } from "@utils"; -import { editUserButtons } from "@utils/buttons"; -import { getTemplateWithDB } from "@utils/db"; import { + FormulaError, + type StatisticalTemplate, + evalCombinaison, + evalOneCombinaison, +} from "@dicelette/core"; +import { ln } from "@dicelette/localization"; +import type { Settings, Translation } from "@dicelette/types"; +import { getTemplateWithDB, getUserNameAndChar } from "database"; +import * as Djs from "discord.js"; +import { registerDmgButton } from "features"; +import { + createStatsEmbed, + displayOldAndNewStats, getEmbeds, getEmbedsList, - parseEmbedFields, + getStatistiqueFields, removeEmbedsFromList, -} from "@utils/parse"; -import * as Djs from "discord.js"; + reply, + sendLogs, +} from "messages"; +import { continueCancelButtons, editUserButtons } from "utils"; + +/** + * Embed to display the statistics when adding a new user + * @param interaction {Djs.ModalSubmitInteraction} + * @param template {StatisticalTemplate} + * @param page {number=2} + * @param lang + */ +export async function registerStatistics( + interaction: Djs.ModalSubmitInteraction, + template: StatisticalTemplate, + page: number | undefined = 2, + lang: Djs.Locale = Djs.Locale.EnglishGB +) { + if (!interaction.message) return; + const ul = ln(lang); + const userEmbed = getEmbeds(ul, interaction.message, "user"); + if (!userEmbed) return; + const statsEmbed = getEmbeds(ul, interaction.message, "stats"); + const { combinaisonFields, stats } = getStatistiqueFields(interaction, template, ul); + //combine all embeds as one + userEmbed.setFooter({ text: ul("common.page", { nb: page }) }); + //add old fields + + const statEmbeds = statsEmbed ?? createStatsEmbed(ul); + for (const [stat, value] of Object.entries(stats)) { + statEmbeds.addFields({ + name: stat.capitalize(), + value: `\`${value}\``, + inline: true, + }); + } + const statsWithoutCombinaison = template.statistics + ? Object.keys(template.statistics) + .filter((stat) => !template.statistics![stat].combinaison) + .map((name) => name.standardize()) + : []; + const embedObject = statEmbeds.toJSON(); + const fields = embedObject.fields; + if (!fields) return; + const parsedFields: { [name: string]: string } = {}; + for (const field of fields) { + parsedFields[field.name.standardize()] = field.value.removeBacktick().standardize(); + } + + const embedStats = Object.fromEntries( + Object.entries(parsedFields).filter(([key]) => statsWithoutCombinaison.includes(key)) + ); + if (Object.keys(embedStats).length === statsWithoutCombinaison.length) { + // noinspection JSUnusedAssignment + let combinaison: { [name: string]: number } = {}; + combinaison = evalCombinaison(combinaisonFields, embedStats); + //add combinaison to the embed + for (const stat of Object.keys(combinaison)) { + statEmbeds.addFields({ + name: stat.capitalize(), + value: `\`${combinaisonFields[stat]}\` = ${combinaison[stat]}`, + inline: true, + }); + } + + await interaction.message.edit({ + embeds: [userEmbed, statEmbeds], + components: [registerDmgButton(ul)], + }); + await reply(interaction, { content: ul("modals.added.stats"), ephemeral: true }); + return; + } + await interaction.message.edit({ + embeds: [userEmbed, statEmbeds], + components: [continueCancelButtons(ul)], + }); + await reply(interaction, { content: ul("modals.added.stats"), ephemeral: true }); + return; +} + /** * Validate the stats and edit the embed with the new stats for editing * @param interaction {Djs.ModalSubmitInteraction} @@ -149,69 +234,3 @@ export async function editStats( }); await sendLogs(`${logMessage}\n${compare}`, interaction.guild as Djs.Guild, db); } - -/** - * Show the stats editor - */ -export async function showEditorStats( - interaction: Djs.ButtonInteraction, - ul: Translation, - db: Settings -) { - const statistics = getEmbeds(ul, interaction.message, "stats"); - if (!statistics) throw new Error(ul("error.statNotFound")); - const stats = parseEmbedFields(statistics.toJSON() as Djs.Embed); - const originalGuildData = db.get(interaction.guild!.id, "templateID.statsName"); - const registeredStats = originalGuildData?.map((stat) => stat.unidecode()); - const userStats = Object.keys(stats).map((stat) => stat.unidecode()); - let statsStrings = ""; - for (const [name, value] of Object.entries(stats)) { - let stringValue = value; - if (!registeredStats?.includes(name.unidecode())) continue; //remove stats that are not registered - if (value.match(/=/)) { - const combinaison = value.split("=")?.[0].trim(); - if (combinaison) stringValue = combinaison; - } - statsStrings += `- ${name}${ul("common.space")}: ${stringValue}\n`; - } - if ( - !isArrayEqual(registeredStats, userStats) && - registeredStats && - registeredStats.length > userStats.length - ) { - //check which stats was added - const diff = registeredStats.filter((x) => !userStats.includes(x)); - for (const stat of diff) { - const realName = originalGuildData?.find((x) => x.unidecode() === stat.unidecode()); - statsStrings += `- ${realName?.capitalize()}${ul("common.space")}: 0\n`; - } - } - - const modal = new Djs.ModalBuilder() - .setCustomId("editStats") - .setTitle(ul("common.statistics").capitalize()); - const input = - new Djs.ActionRowBuilder().addComponents( - new Djs.TextInputBuilder() - .setCustomId("allStats") - .setLabel(ul("modals.edit.stats")) - .setRequired(true) - .setStyle(Djs.TextInputStyle.Paragraph) - .setValue(statsStrings) - ); - modal.addComponents(input); - await interaction.showModal(modal); -} - -/** - * The button that trigger the stats editor - */ -export async function triggerEditStats( - interaction: Djs.ButtonInteraction, - ul: Translation, - interactionUser: Djs.User, - db: Settings -) { - if (await allowEdit(interaction, db, interactionUser)) - showEditorStats(interaction, ul, db); -} diff --git a/packages/bot/src/features/user/index.ts b/packages/bot/src/features/user/index.ts new file mode 100644 index 00000000..49bc015b --- /dev/null +++ b/packages/bot/src/features/user/index.ts @@ -0,0 +1,3 @@ +export * from "./modals"; +export * from "./validation"; +export * from "./register"; diff --git a/src/interactions/register/start.ts b/packages/bot/src/features/user/modals.ts similarity index 51% rename from src/interactions/register/start.ts rename to packages/bot/src/features/user/modals.ts index 5018da8b..f3ddbf48 100644 --- a/src/interactions/register/start.ts +++ b/packages/bot/src/features/user/modals.ts @@ -1,84 +1,22 @@ import type { StatisticalTemplate } from "@dicelette/core"; -import { createStatsEmbed } from "@interactions"; -import { embedStatistiques, showStatistiqueModal } from "@interactions/add/stats"; -import type { Settings, Translation } from "@interfaces/discord"; -import { createEmbedFirstPage } from "@register/validate"; -import { embedError, reply } from "@utils"; -import { getTemplateWithDB } from "@utils/db"; -import { getEmbeds, parseEmbedFields } from "@utils/parse"; +import type { Translation } from "@dicelette/types"; import * as Djs from "discord.js"; -/** - * Interaction to continue to the next page of the statistics when registering a new user - */ -export async function continuePage( +import { reply } from "messages"; + +export async function startRegisterUser( interaction: Djs.ButtonInteraction, - dbTemplate: StatisticalTemplate, + template: StatisticalTemplate, + interactionUser: Djs.User, ul: Translation, - interactionUser: Djs.User + havePrivate?: boolean ) { const isModerator = interaction.guild?.members.cache .get(interactionUser.id) ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); - if (!isModerator) { - await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); - return; - } - const page = Number.isNaN(Number.parseInt(interaction.customId.replace("page", ""), 10)) - ? 1 - : Number.parseInt(interaction.customId.replace("page", ""), 10); - const embed = getEmbeds(ul, interaction.message, "user"); - if (!embed || !dbTemplate.statistics) return; - const statsEmbed = getEmbeds(ul, interaction.message, "stats") ?? createStatsEmbed(ul); - const allTemplateStat = Object.keys(dbTemplate.statistics).map((stat) => - stat.unidecode() - ); - - const statsAlreadySet = Object.keys(parseEmbedFields(statsEmbed.toJSON() as Djs.Embed)) - .filter((stat) => allTemplateStat.includes(stat.unidecode())) - .map((stat) => stat.unidecode()); - if (statsAlreadySet.length === allTemplateStat.length) { - await reply(interaction, { content: ul("modals.alreadySet"), ephemeral: true }); - return; - } - await showStatistiqueModal(interaction, dbTemplate, statsAlreadySet, page + 1); + if (isModerator) await showFirstPageModal(interaction, template, ul, havePrivate); + else await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); } -/** - * Register the statistic in the embed when registering a new user and validate the modal - * Also verify if the template is registered before embedding the statistics - */ -export async function pageNumber( - interaction: Djs.ModalSubmitInteraction, - ul: Translation, - db: Settings -) { - const pageNumber = Number.parseInt(interaction.customId.replace("page", ""), 10); - if (Number.isNaN(pageNumber)) return; - const template = await getTemplateWithDB(interaction, db); - if (!template) { - await reply(interaction, { embeds: [embedError(ul("error.noTemplate"), ul)] }); - return; - } - await embedStatistiques( - interaction, - template, - pageNumber, - db.get(interaction.guild!.id, "lang") ?? interaction.locale - ); -} -/** - * Submit the first page when the modal is validated - */ -export async function recordFirstPage( - interaction: Djs.ModalSubmitInteraction, - db: Settings -) { - if (!interaction.guild || !interaction.channel || interaction.channel.isDMBased()) - return; - const template = await getTemplateWithDB(interaction, db); - if (!template) return; - await createEmbedFirstPage(interaction, template, db); -} /** * Modal opened to register a new user with the name of the character and the user id */ @@ -154,20 +92,3 @@ export async function showFirstPageModal( modal.addComponents(components); await interaction.showModal(modal); } - -/** - * Open the showFirstPageModal function if the user is a moderator - */ -export async function startRegisterUser( - interaction: Djs.ButtonInteraction, - template: StatisticalTemplate, - interactionUser: Djs.User, - ul: Translation, - havePrivate?: boolean -) { - const isModerator = interaction.guild?.members.cache - .get(interactionUser.id) - ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); - if (isModerator) await showFirstPageModal(interaction, template, ul, havePrivate); - else await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); -} diff --git a/packages/bot/src/features/user/register.ts b/packages/bot/src/features/user/register.ts new file mode 100644 index 00000000..14377cda --- /dev/null +++ b/packages/bot/src/features/user/register.ts @@ -0,0 +1,132 @@ +import type { StatisticalTemplate } from "@dicelette/core"; +import { ln } from "@dicelette/localization"; +import type { Settings, Translation } from "@dicelette/types"; +import { NoChannel, verifyAvatarUrl } from "@dicelette/utils"; +import { getTemplateWithDB } from "database"; +import * as Djs from "discord.js"; +import { registerDmgButton, registerStatistics } from "features"; +import { embedError, reply } from "messages"; +import { continueCancelButtons, isUserNameOrId } from "utils"; + +/** + * Register the statistic in the embed when registering a new user and validate the modal + * Also verify if the template is registered before embedding the statistics + */ +export async function pageNumber( + interaction: Djs.ModalSubmitInteraction, + ul: Translation, + db: Settings +) { + const pageNumber = Number.parseInt(interaction.customId.replace("page", ""), 10); + if (Number.isNaN(pageNumber)) return; + const template = await getTemplateWithDB(interaction, db); + if (!template) { + await reply(interaction, { embeds: [embedError(ul("error.noTemplate"), ul)] }); + return; + } + await registerStatistics( + interaction, + template, + pageNumber, + db.get(interaction.guild!.id, "lang") ?? interaction.locale + ); +} + +/** + * Submit the first page when the modal is validated + */ +export async function recordFirstPage( + interaction: Djs.ModalSubmitInteraction, + db: Settings +) { + if (!interaction.guild || !interaction.channel || interaction.channel.isDMBased()) + return; + const template = await getTemplateWithDB(interaction, db); + if (!template) return; + await createEmbedFirstPage(interaction, template, db); +} + +/** + * Create the embed after registering the user + * If the template has statistics, show the continue button + * Else show the dice button + */ +export async function createEmbedFirstPage( + interaction: Djs.ModalSubmitInteraction, + template: StatisticalTemplate, + setting: Settings +) { + const lang = setting.get(interaction.guild!.id, "lang") ?? interaction.locale; + const ul = ln(lang); + const channel = interaction.channel; + if (!channel) { + throw new NoChannel(); + } + const userFromField = interaction.fields.getTextInputValue("userID"); + const user = await isUserNameOrId(userFromField, interaction); + if (!user) { + await reply(interaction, { + embeds: [embedError(ul("error.user"), ul)], + ephemeral: true, + }); + return; + } + const customChannel = interaction.fields.getTextInputValue("channelId"); + const charName = interaction.fields.getTextInputValue("charName"); + const isPrivate = + interaction.fields.getTextInputValue("private")?.toLowerCase() === "x"; + const avatar = interaction.fields.getTextInputValue("avatar"); + + let sheetId = setting.get(interaction.guild!.id, "managerId"); + const privateChannel = setting.get(interaction.guild!.id, "privateChannel"); + if (isPrivate && privateChannel) sheetId = privateChannel; + if (customChannel.length > 0) sheetId = customChannel; + + const verifiedAvatar = verifyAvatarUrl(avatar); + const existChannel = sheetId + ? await interaction.guild?.channels.fetch(sheetId) + : undefined; + if (!existChannel) { + await reply(interaction, { + embeds: [embedError(ul("error.noThread"), ul)], + ephemeral: true, + }); + return; + } + const embed = new Djs.EmbedBuilder() + .setTitle(ul("embed.add")) + .setThumbnail(verifiedAvatar ? avatar : user.displayAvatarURL()) + .setFooter({ text: ul("common.page", { nb: 1 }) }) + .addFields( + { + name: ul("common.charName"), + value: charName.length > 0 ? charName : ul("common.noSet"), + inline: true, + }, + { name: ul("common.user"), value: Djs.userMention(user.id), inline: true }, + { name: ul("common.isPrivate"), value: isPrivate ? "✓" : "✕", inline: true } + ); + if (sheetId) { + embed.addFields({ name: "_ _", value: "_ _", inline: true }); + embed.addFields({ + name: ul("common.channel").capitalize(), + value: `${Djs.channelMention(sheetId as string)}`, + inline: true, + }); + embed.addFields({ name: "_ _", value: "_ _", inline: true }); + } + + //add continue button + if (template.statistics) { + await reply(interaction, { + content: verifiedAvatar + ? "" + : `:warning: **${ul("error.avatar.url")}** *${ul("edit_avatar.default")}*`, + embeds: [embed], + components: [continueCancelButtons(ul)], + }); + return; + } + const allButtons = registerDmgButton(ul); + await reply(interaction, { embeds: [embed], components: [allButtons] }); +} diff --git a/src/interactions/register/validate.ts b/packages/bot/src/features/user/validation.ts similarity index 62% rename from src/interactions/register/validate.ts rename to packages/bot/src/features/user/validation.ts index adce519b..0eb112ad 100644 --- a/src/interactions/register/validate.ts +++ b/packages/bot/src/features/user/validation.ts @@ -1,112 +1,76 @@ import type { StatisticalTemplate } from "@dicelette/core"; +import { ln } from "@dicelette/localization"; +import type { UserData } from "@dicelette/types"; +import type { Settings, Translation } from "@dicelette/types"; +import { NoEmbed, logger } from "@dicelette/utils"; +import * as Djs from "discord.js"; +import { showStatistiqueModal } from "features"; import { createDiceEmbed, + createEmbedsList, createStatsEmbed, createTemplateEmbed, createUserEmbed, -} from "@interactions"; -import type { UserData } from "@interfaces/database"; -import type { Settings, Translation } from "@interfaces/discord"; -import { ln } from "@localization"; -import { logger } from "@logger"; -import { - NoChannel, - NoEmbed, - addAutoRole, embedError, + getEmbeds, + parseEmbedFields, reply, repostInThread, -} from "@utils"; -import { continueCancelButtons, registerDmgButton } from "@utils/buttons"; -import { isUserNameOrId } from "@utils/find"; -import { createEmbedsList, getEmbeds, parseEmbedFields } from "@utils/parse"; -import * as Djs from "discord.js"; -export function verifyAvatarUrl(url: string) { - if (url.length === 0) return false; - if (url.match(/^(https:)([\/|.\w\s\-_])*(?:jpe?g|gifv?|png|webp)$/gi)) return url; - return false; -} +} from "messages"; +import { addAutoRole } from "utils"; /** - * Create the embed after registering the user - * If the template has statistics, show the continue button - * Else show the dice button + * Interaction to continue to the next page of the statistics when registering a new user */ -export async function createEmbedFirstPage( - interaction: Djs.ModalSubmitInteraction, - template: StatisticalTemplate, - setting: Settings +export async function continuePage( + interaction: Djs.ButtonInteraction, + dbTemplate: StatisticalTemplate, + ul: Translation, + interactionUser: Djs.User ) { - const lang = setting.get(interaction.guild!.id, "lang") ?? interaction.locale; - const ul = ln(lang); - const channel = interaction.channel; - if (!channel) { - throw new NoChannel(); - } - const userFromField = interaction.fields.getTextInputValue("userID"); - const user = await isUserNameOrId(userFromField, interaction); - if (!user) { - reply(interaction, { embeds: [embedError(ul("error.user"), ul)], ephemeral: true }); + const isModerator = interaction.guild?.members.cache + .get(interactionUser.id) + ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); + if (!isModerator) { + await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); return; } - const customChannel = interaction.fields.getTextInputValue("channelId"); - const charName = interaction.fields.getTextInputValue("charName"); - const isPrivate = - interaction.fields.getTextInputValue("private")?.toLowerCase() === "x"; - const avatar = interaction.fields.getTextInputValue("avatar"); - - let sheetId = setting.get(interaction.guild!.id, "managerId"); - const privateChannel = setting.get(interaction.guild!.id, "privateChannel"); - if (isPrivate && privateChannel) sheetId = privateChannel; - if (customChannel.length > 0) sheetId = customChannel; + const page = Number.isNaN(Number.parseInt(interaction.customId.replace("page", ""), 10)) + ? 1 + : Number.parseInt(interaction.customId.replace("page", ""), 10); + const embed = getEmbeds(ul, interaction.message, "user"); + if (!embed || !dbTemplate.statistics) return; + const statsEmbed = getEmbeds(ul, interaction.message, "stats") ?? createStatsEmbed(ul); + const allTemplateStat = Object.keys(dbTemplate.statistics).map((stat) => + stat.unidecode() + ); - const verifiedAvatar = verifyAvatarUrl(avatar); - const existChannel = sheetId - ? await interaction.guild?.channels.fetch(sheetId) - : undefined; - if (!existChannel) { - reply(interaction, { - embeds: [embedError(ul("error.noThread"), ul)], - ephemeral: true, - }); + const statsAlreadySet = Object.keys(parseEmbedFields(statsEmbed.toJSON() as Djs.Embed)) + .filter((stat) => allTemplateStat.includes(stat.unidecode())) + .map((stat) => stat.unidecode()); + if (statsAlreadySet.length === allTemplateStat.length) { + await reply(interaction, { content: ul("modals.alreadySet"), ephemeral: true }); return; } - const embed = new Djs.EmbedBuilder() - .setTitle(ul("embed.add")) - .setThumbnail(verifiedAvatar ? avatar : user.displayAvatarURL()) - .setFooter({ text: ul("common.page", { nb: 1 }) }) - .addFields( - { - name: ul("common.charName"), - value: charName.length > 0 ? charName : ul("common.noSet"), - inline: true, - }, - { name: ul("common.user"), value: Djs.userMention(user.id), inline: true }, - { name: ul("common.isPrivate"), value: isPrivate ? "✓" : "✕", inline: true } - ); - if (sheetId) { - embed.addFields({ name: "_ _", value: "_ _", inline: true }); - embed.addFields({ - name: ul("common.channel").capitalize(), - value: `${Djs.channelMention(sheetId as string)}`, - inline: true, - }); - embed.addFields({ name: "_ _", value: "_ _", inline: true }); - } + await showStatistiqueModal(interaction, dbTemplate, statsAlreadySet, page + 1); +} - //add continue button - if (template.statistics) { - await reply(interaction, { - content: verifiedAvatar - ? "" - : `:warning: **${ul("error.avatar.url")}** *${ul("edit_avatar.default")}*`, - embeds: [embed], - components: [continueCancelButtons(ul)], - }); - return; - } - const allButtons = registerDmgButton(ul); - await reply(interaction, { embeds: [embed], components: [allButtons] }); +/** + * Validate the user and create the embeds when the button is clicked + */ + +export async function validateUserButton( + interaction: Djs.ButtonInteraction, + interactionUser: Djs.User, + template: StatisticalTemplate, + ul: Translation, + db: Settings +) { + const isModerator = interaction.guild?.members.cache + .get(interactionUser.id) + ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); + if (isModerator) await validateUser(interaction, template, db); + else await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); } /** @@ -220,13 +184,14 @@ export async function validateUser( template: { diceType: template.diceType, critical: template.critical, + customCritical: template.customCritical, }, damage: templateDamage, private: isPrivate, avatar: userEmbed.toJSON().thumbnail?.url, }; let templateEmbed: Djs.EmbedBuilder | undefined = undefined; - if (template.diceType || template.critical) { + if (template.diceType || template.critical || template.customCritical) { templateEmbed = createTemplateEmbed(ul); if (template.diceType) templateEmbed.addFields({ @@ -248,6 +213,17 @@ export async function validateUser( inline: true, }); } + const criticalTemplate = template.customCritical ?? {}; + for (const [name, value] of Object.entries(criticalTemplate)) { + const nameCritical = value.onNaturalDice + ? `(N) ${name.capitalize()}` + : name.capitalize(); + templateEmbed.addFields({ + name: nameCritical, + value: `\`${value.sign} ${value.value}\``, + inline: true, + }); + } } const allEmbeds = createEmbedsList(userDataEmbed, statsEmbed, diceEmbed, templateEmbed); await repostInThread( @@ -269,21 +245,3 @@ export async function validateUser( await reply(interaction, { content: ul("modals.finished"), ephemeral: true }); return; } - -/** - * Validate the user and create the embeds when the button is clicked - */ - -export async function validateUserButton( - interaction: Djs.ButtonInteraction, - interactionUser: Djs.User, - template: StatisticalTemplate, - ul: Translation, - db: Settings -) { - const isModerator = interaction.guild?.members.cache - .get(interactionUser.id) - ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); - if (isModerator) await validateUser(interaction, template, db); - else await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); -} diff --git a/packages/bot/src/locales.ts b/packages/bot/src/locales.ts new file mode 100644 index 00000000..ebfd9846 --- /dev/null +++ b/packages/bot/src/locales.ts @@ -0,0 +1,18 @@ +import { LocalePrimary, resources } from "@dicelette/localization"; +import * as Djs from "discord.js"; + +export const localeList = Object.keys(Djs.Locale) + .map((key) => { + return { + name: key, + value: Djs.Locale[key as keyof typeof Djs.Locale], + }; + }) + .filter((x) => Object.keys(resources).includes(x.value)) + .map((x) => { + return { + name: LocalePrimary[x.name as keyof typeof LocalePrimary], + value: x.value as Djs.Locale, + }; + }); +localeList.push({ name: LocalePrimary.English, value: Djs.Locale.EnglishUS }); diff --git a/packages/bot/src/messages/bulk.ts b/packages/bot/src/messages/bulk.ts new file mode 100644 index 00000000..f6af063b --- /dev/null +++ b/packages/bot/src/messages/bulk.ts @@ -0,0 +1,125 @@ +import type { StatisticalTemplate } from "@dicelette/core"; +import type { PersonnageIds } from "@dicelette/types"; +import type { Settings, Translation } from "@dicelette/types"; +import { logger } from "@dicelette/utils"; +import * as Djs from "discord.js"; +import { createTemplateEmbed, getEmbeds, getEmbedsList } from "messages"; +import { searchUserChannel } from "utils"; + +/** + * Update the template of existing user when the template is edited by moderation + * @param guildData {GuildData} + * @param interaction {Djs.CommandInteraction} + * @param ul {Translation} + * @param template {StatisticalTemplate} + */ +export async function bulkEditTemplateUser( + guildData: Settings, + interaction: Djs.CommandInteraction, + ul: Translation, + template: StatisticalTemplate +) { + const users = guildData.get(interaction.guild!.id, "user"); + + for (const userID in users) { + for (const char of users[userID]) { + const sheetLocation: PersonnageIds = { + channelId: char.messageId[1], + messageId: char.messageId[0], + }; + const thread = await searchUserChannel( + guildData, + interaction, + ul, + sheetLocation.channelId + ); + if (!thread) continue; + try { + const userMessages = await thread.messages.fetch(sheetLocation.messageId); + const templateEmbed = getEmbeds(ul, userMessages, "template"); + if (!templateEmbed) continue; + const newEmbed = createTemplateEmbed(ul); + if (template.diceType) + newEmbed.addFields({ + name: ul("common.dice"), + value: `\`${template.diceType}\``, + inline: true, + }); + if (template.critical?.success) + newEmbed.addFields({ + name: ul("roll.critical.success"), + value: `\`${template.critical.success}\``, + inline: true, + }); + if (template.critical?.failure) + newEmbed.addFields({ + name: ul("roll.critical.failure"), + value: `\`${template.critical.failure}\``, + inline: true, + }); + const listEmbed = getEmbedsList( + ul, + { which: "template", embed: newEmbed }, + userMessages + ); + await userMessages.edit({ embeds: listEmbed.list }); + } catch { + //pass + } + } + } +} + +/** + * Delete all characters from the guild + * @param guildData {Settings} + * @param interaction {Djs.CommandInteraction} + * @param ul {Translation} + */ +export async function bulkDeleteCharacters( + guildData: Settings, + interaction: Djs.CommandInteraction, + ul: Translation +) { + //first add a warning using buttons + const msg = ul("register.delete.confirm"); + const embed = new Djs.EmbedBuilder() + .setTitle(ul("deleteChar.confirm.title")) + .setDescription(msg) + .setColor(Djs.Colors.Red); + const confirm = new Djs.ButtonBuilder() + .setCustomId("delete_all_confirm") + .setStyle(Djs.ButtonStyle.Danger) + .setLabel(ul("common.confirm")); + const cancel = new Djs.ButtonBuilder() + .setCustomId("delete_all_cancel") + .setStyle(Djs.ButtonStyle.Secondary) + .setLabel(ul("common.cancel")); + const row = new Djs.ActionRowBuilder().addComponents( + confirm, + cancel + ); + const channel = interaction.channel as Djs.TextChannel; + const rep = await channel.send({ embeds: [embed], components: [row] }); + const collectorFilter = (i: { user: { id: string | undefined } }) => + i.user.id === interaction.user.id; + try { + const confirm = await rep.awaitMessageComponent({ + filter: collectorFilter, + time: 60_000, + }); + if (confirm.customId === "delete_all_confirm") { + guildData.delete(interaction.guild!.id, "user"); + await rep.edit({ + components: [], + content: ul("register.delete.done"), + embeds: [], + }); + } else { + await rep.edit({ components: [] }); + } + } catch (err) { + logger.error(err); + } + return; +} diff --git a/src/utils/parse.ts b/packages/bot/src/messages/embeds.ts similarity index 58% rename from src/utils/parse.ts rename to packages/bot/src/messages/embeds.ts index 71939b96..4f3c66d7 100644 --- a/src/utils/parse.ts +++ b/packages/bot/src/messages/embeds.ts @@ -1,22 +1,24 @@ import type { StatisticalTemplate } from "@dicelette/core"; -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { createTemplateEmbed } from "@interactions"; -import type { PersonnageIds } from "@interfaces/database"; -import type { Settings, Translation } from "@interfaces/discord"; -import { findln } from "@localization"; -import { logger } from "@logger"; -import { NoEmbed, searchUserChannel } from "@utils"; +import { findln } from "@dicelette/localization"; +import type { Translation } from "@dicelette/types"; +import { NoEmbed } from "@dicelette/utils"; import * as Djs from "discord.js"; -/** - * Ensure the embeds are present - * @param {Message} message - */ export function ensureEmbed(message?: Djs.Message) { const oldEmbeds = message?.embeds[0]; if (!oldEmbeds || !oldEmbeds?.fields) throw new NoEmbed(); return oldEmbeds; } +export const embedError = (error: string, ul: Translation, cause?: string) => { + const embed = new Djs.EmbedBuilder() + .setDescription(error) + .setColor("Red") + .setAuthor({ name: ul("common.error"), iconURL: "https://i.imgur.com/2ulUJCc.png" }) + .setTimestamp(); + if (cause) embed.setFooter({ text: cause }); + return embed; +}; + /** * Create a list of embeds */ @@ -73,41 +75,6 @@ export function getEmbedsList( }; } -/** - * Remove the embeds from the list - */ -export function removeEmbedsFromList( - embeds: Djs.EmbedBuilder[], - which: "user" | "stats" | "damage" | "template" -) { - return embeds.filter((embed) => { - const embedTitle = embed.toJSON().title; - if (!embedTitle) return false; - const title = findln(embedTitle); - if (which === "user") - return title !== "embed.user" && title !== "embed.add" && title !== "embed.old"; - if (which === "stats") - return title !== "common.statistic" && title !== "common.statistics"; - if (which === "damage") return title !== "embed.dice"; - if (which === "template") return title !== "embed.template"; - }); -} - -/** - * Parse the embed fields and remove the backtick if any - */ -export function parseEmbedFields(embed: Djs.Embed): { [name: string]: string } { - const fields = embed.fields; - if (!fields) return {}; - const parsedFields: { [name: string]: string } = {}; - for (const field of fields) { - parsedFields[findln(field.name.removeBacktick())] = findln( - field.value.removeBacktick() - ); - } - return parsedFields; -} - /** * Get the embeds from the message and recreate it as EmbedBuilder */ @@ -135,122 +102,81 @@ export function getEmbeds( } /** - * Update the template of existing user when the template is edited by moderation - * @param guildData {GuildData} - * @param interaction {Djs.CommandInteraction} + * Parse the embed fields and remove the backtick if any + */ +export function parseEmbedFields(embed: Djs.Embed): { [name: string]: string } { + const fields = embed.fields; + if (!fields) return {}; + const parsedFields: { [name: string]: string } = {}; + for (const field of fields) { + parsedFields[findln(field.name.removeBacktick().unidecode(true))] = findln( + field.value.removeBacktick() + ); + } + return parsedFields; +} + +/** + * Create the userEmbed and embedding the avatar user in the thumbnail * @param ul {Translation} - * @param template {StatisticalTemplate} + * @param thumbnail {string} The avatar of the user in the server (use server profile first, after global avatar) + * @param user + * @param charName */ -export async function bulkEditTemplateUser( - guildData: Settings, - interaction: Djs.CommandInteraction, +export function createUserEmbed( ul: Translation, - template: StatisticalTemplate + thumbnail: string | null, + user: string, + charName?: string ) { - const users = guildData.get(interaction.guild!.id, "user"); + const userEmbed = new Djs.EmbedBuilder() + .setTitle(ul("embed.user")) + .setColor("Random") + .setThumbnail(thumbnail) + .addFields({ + name: ul("common.user").capitalize(), + value: `<@${user}>`, + inline: true, + }); + if (charName) + userEmbed.addFields({ + name: ul("common.character").capitalize(), + value: charName.capitalize(), + inline: true, + }); + else + userEmbed.addFields({ + name: ul("common.character").capitalize(), + value: ul("common.noSet").capitalize(), + inline: true, + }); + return userEmbed; +} - for (const userID in users) { - for (const char of users[userID]) { - const sheetLocation: PersonnageIds = { - channelId: char.messageId[1], - messageId: char.messageId[0], - }; - const thread = await searchUserChannel( - guildData, - interaction, - ul, - sheetLocation.channelId - ); - if (!thread) continue; - try { - const userMessages = await thread.messages.fetch(sheetLocation.messageId); - const templateEmbed = getEmbeds(ul, userMessages, "template"); - if (!templateEmbed) continue; - const newEmbed = createTemplateEmbed(ul); - if (template.diceType) - newEmbed.addFields({ - name: ul("common.dice"), - value: `\`${template.diceType}\``, - inline: true, - }); - if (template.critical?.success) - newEmbed.addFields({ - name: ul("roll.critical.success"), - value: `\`${template.critical.success}\``, - inline: true, - }); - if (template.critical?.failure) - newEmbed.addFields({ - name: ul("roll.critical.failure"), - value: `\`${template.critical.failure}\``, - inline: true, - }); - const listEmbed = getEmbedsList( - ul, - { which: "template", embed: newEmbed }, - userMessages - ); - await userMessages.edit({ embeds: listEmbed.list }); - } catch { - //pass - } - } - } +/** + * Create the statistic embed + * @param ul {Translation} + */ +export function createStatsEmbed(ul: Translation) { + return new Djs.EmbedBuilder() + .setTitle(ul("common.statistics").capitalize()) + .setColor("Aqua"); } /** - * Delete all characters from the guild - * @param guildData {Settings} - * @param interaction {Djs.CommandInteraction} + * Create the template embed for user * @param ul {Translation} */ -export async function bulkDeleteCharacters( - guildData: Settings, - interaction: Djs.CommandInteraction, - ul: Translation -) { - //first add a warning using buttons - const msg = ul("register.delete.confirm"); - const embed = new Djs.EmbedBuilder() - .setTitle(ul("deleteChar.confirm.title")) - .setDescription(msg) - .setColor(Djs.Colors.Red); - const confirm = new Djs.ButtonBuilder() - .setCustomId("delete_all_confirm") - .setStyle(Djs.ButtonStyle.Danger) - .setLabel(ul("common.confirm")); - const cancel = new Djs.ButtonBuilder() - .setCustomId("delete_all_cancel") - .setStyle(Djs.ButtonStyle.Secondary) - .setLabel(ul("common.cancel")); - const row = new Djs.ActionRowBuilder().addComponents( - confirm, - cancel - ); - const channel = interaction.channel as Djs.TextChannel; - const rep = await channel.send({ embeds: [embed], components: [row] }); - const collectorFilter = (i: { user: { id: string | undefined } }) => - i.user.id === interaction.user.id; - try { - const confirm = await rep.awaitMessageComponent({ - filter: collectorFilter, - time: 60_000, - }); - console.log(confirm); - if (confirm.customId === "delete_all_confirm") { - guildData.delete(interaction.guild!.id, "user"); - await rep.edit({ - components: [], - content: ul("register.delete.done"), - embeds: [], - }); - } else { - await rep.edit({ components: [] }); - } - } catch (err) { - logger.error(err); - } - return; +export function createTemplateEmbed(ul: Translation) { + return new Djs.EmbedBuilder().setTitle(ul("embed.template")).setColor("DarkGrey"); +} + +/** + * Create the dice skill embed + * @param ul {Translation} + */ +export function createDiceEmbed(ul: Translation) { + return new Djs.EmbedBuilder().setTitle(ul("embed.dice")).setColor("Green"); } /** @@ -290,3 +216,23 @@ export function getStatistiqueFields( } return { combinaisonFields, stats }; } + +/** + * Remove the embeds from the list + */ +export function removeEmbedsFromList( + embeds: Djs.EmbedBuilder[], + which: "user" | "stats" | "damage" | "template" +) { + return embeds.filter((embed) => { + const embedTitle = embed.toJSON().title; + if (!embedTitle) return false; + const title = findln(embedTitle); + if (which === "user") + return title !== "embed.user" && title !== "embed.add" && title !== "embed.old"; + if (which === "stats") + return title !== "common.statistic" && title !== "common.statistics"; + if (which === "damage") return title !== "embed.dice"; + if (which === "template") return title !== "embed.template"; + }); +} diff --git a/packages/bot/src/messages/index.ts b/packages/bot/src/messages/index.ts new file mode 100644 index 00000000..6688ce76 --- /dev/null +++ b/packages/bot/src/messages/index.ts @@ -0,0 +1,23 @@ +import type { DiscordTextChannel } from "@dicelette/types"; +import type * as Djs from "discord.js"; +export async function findMessageBefore( + channel: DiscordTextChannel, + inter: Djs.Message | Djs.InteractionResponse, + client: Djs.Client +) { + let messagesBefore = await channel.messages.fetch({ before: inter.id, limit: 1 }); + let messageBefore = messagesBefore.first(); + while (messageBefore && messageBefore.author.username === client.user?.username) { + messagesBefore = await channel.messages.fetch({ + before: messageBefore.id, + limit: 1, + }); + messageBefore = messagesBefore.first(); + } + return messageBefore; +} + +export * from "./embeds"; +export * from "./send"; +export * from "./thread"; +export * from "./bulk"; diff --git a/packages/bot/src/messages/send.ts b/packages/bot/src/messages/send.ts new file mode 100644 index 00000000..b274eb6a --- /dev/null +++ b/packages/bot/src/messages/send.ts @@ -0,0 +1,75 @@ +import type { Settings } from "@dicelette/types"; +import type * as Djs from "discord.js"; + +export async function sendLogs(message: string, guild: Djs.Guild, db: Settings) { + const guildData = db.get(guild.id); + if (!guildData?.logs) return; + const channel = guildData.logs; + try { + const channelToSend = (await guild.channels.fetch(channel)) as Djs.TextChannel; + await channelToSend.send(message); + } catch (error) { + return; + } +} +export async function reply( + interaction: + | Djs.CommandInteraction + | Djs.ModalSubmitInteraction + | Djs.ButtonInteraction + | Djs.StringSelectMenuInteraction, + options: string | Djs.InteractionReplyOptions | Djs.MessagePayload +) { + return interaction.replied || interaction.deferred + ? await interaction.editReply(options) + : await interaction.reply(options); +} + +/** + * Deletes a given message after a specified time delay. + * If the time delay is zero, the function exits immediately. + * Uses setTimeout to schedule the deletion and handles any errors silently. + * @param message - An instance of InteractionResponse or Message that needs to be deleted. + * @param time - A number representing the delay in milliseconds before the message is deleted. + */ +export async function deleteAfter( + message: Djs.InteractionResponse | Djs.Message, + time: number +): Promise { + if (time === 0) return; + + setTimeout(async () => { + try { + await message.delete(); + } catch (error) { + // Can't delete message, probably because the message was already deleted; ignoring the error. + } + }, time); +} + +export function displayOldAndNewStats( + oldStats?: Djs.APIEmbedField[], + newStats?: Djs.APIEmbedField[] +) { + let stats = ""; + if (oldStats && newStats) { + for (const field of oldStats) { + const name = field.name.toLowerCase(); + const newField = newStats.find((f) => f.name.toLowerCase() === name); + if (!newField) { + stats += `- ~~${field.name}: ${field.value}~~\n`; + continue; + } + if (field.value === newField.value) continue; + stats += `- ${field.name}: ${field.value} ⇒ ${newField.value}\n`; + } + //verify if there is new stats + for (const field of newStats) { + const name = field.name.toLowerCase(); + if (!oldStats.find((f) => f.name.toLowerCase() === name)) { + stats += `- ${field.name}: 0 ⇒ ${field.value}\n`; + } + } + } + return stats; +} diff --git a/packages/bot/src/messages/thread.ts b/packages/bot/src/messages/thread.ts new file mode 100644 index 00000000..48746de2 --- /dev/null +++ b/packages/bot/src/messages/thread.ts @@ -0,0 +1,332 @@ +import type { + CharDataWithName, + CharacterData, + PersonnageIds, + Settings, + Translation, + UserData, + UserRegistration, +} from "@dicelette/types"; +import type { EClient } from "client"; +import { registerUser, setDefaultManagerId } from "database"; +import * as Djs from "discord.js"; +import { deleteAfter, embedError, reply, sendLogs } from "messages"; +import { editUserButtons, haveAccess, searchUserChannel, selectEditMenu } from "utils"; + +export async function createDefaultThread( + parent: Djs.ThreadChannel | Djs.TextChannel, + guildData: Settings, + interaction: Djs.BaseInteraction, + save = true +) { + if (parent instanceof Djs.ThreadChannel) parent = parent.parent as Djs.TextChannel; + let thread = (await parent.threads.fetch()).threads.find( + (thread) => thread.name === "📝 • [STATS]" + ) as Djs.AnyThreadChannel | undefined; + if (!thread) { + thread = (await parent.threads.create({ + name: "📝 • [STATS]", + autoArchiveDuration: 10080, + })) as Djs.AnyThreadChannel; + if (save) setDefaultManagerId(guildData, interaction, thread.id); + } + return thread; +} + +/** + * Set the tags for thread channel in forum + */ +export async function setTagsForRoll(forum: Djs.ForumChannel) { + //check if the tags `🪡 roll logs` exists + const allTags = forum.availableTags; + const diceRollTag = allTags.find( + (tag) => tag.name === "Dice Roll" && tag.emoji?.name === "🪡" + ); + if (diceRollTag) return diceRollTag; + + const availableTags: Djs.GuildForumTagData[] = allTags.map((tag) => { + return { + id: tag.id, + moderated: tag.moderated, + name: tag.name, + emoji: tag.emoji, + }; + }); + availableTags.push({ + name: "Dice Roll", + emoji: { id: null, name: "🪡" }, + }); + await forum.setAvailableTags(availableTags); + + return forum.availableTags.find( + (tag) => tag.name === "Dice Roll" && tag.emoji?.name === "🪡" + ) as Djs.GuildForumTagData; +} + +/** + * Repost the character sheet in the thread / channel selected with `guildData.managerId` + */ +export async function repostInThread( + embed: Djs.EmbedBuilder[], + interaction: Djs.BaseInteraction, + userTemplate: UserData, + userId: string, + ul: Translation, + which: { stats?: boolean; dice?: boolean; template?: boolean }, + guildData: Settings, + threadId: string +) { + userTemplate.userName = userTemplate.userName + ? userTemplate.userName.toLowerCase() + : undefined; + const damageName = userTemplate.damage ? Object.keys(userTemplate.damage) : undefined; + const channel = interaction.channel; + // noinspection SuspiciousTypeOfGuard + if (!channel || channel instanceof Djs.CategoryChannel) return; + if (!guildData) + throw new Error( + ul("error.generic.e", { + e: "No server data found in database for this server.", + }) + ); + const dataToSend = { + embeds: embed, + components: [editUserButtons(ul, which.stats, which.dice), selectEditMenu(ul)], + }; + let isForumThread = false; + let thread = await searchUserChannel(guildData, interaction, ul, threadId, true); + let msg: Djs.Message | undefined = undefined; + if (!thread) { + const channel = await interaction.guild?.channels.fetch(threadId); + // noinspection SuspiciousTypeOfGuard + if (channel && channel instanceof Djs.ForumChannel) { + const userName = + userTemplate.userName ?? + (await interaction.guild?.members.fetch(userId))?.displayName; + //create a new thread in the forum + const newThread = await channel.threads.create({ + name: userName ?? `${ul("common.sheet")} ${ul("common.character").toUpperCase()}`, + autoArchiveDuration: Djs.ThreadAutoArchiveDuration.OneWeek, + message: dataToSend, + }); + thread = newThread as Djs.AnyThreadChannel; + isForumThread = true; + const starterMsg = await newThread.fetchStarterMessage(); + if (!starterMsg) throw new Error(ul("error.noThread")); + msg = starterMsg; + const ping = await thread.send( + interaction.user.id !== userId + ? `<@${interaction.user.id}> || <@${userId}>` + : `<@${interaction.user.id}>` + ); + await deleteAfter(ping, 5000); + } + } else { + // noinspection SuspiciousTypeOfGuard + if (!thread && channel instanceof Djs.TextChannel) + thread = await createDefaultThread(channel, guildData, interaction); + } + if (!thread) { + throw new Error(ul("error.noThread")); + } + if (!isForumThread) msg = await thread.send(dataToSend); + if (!msg) throw new Error(ul("error.noThread")); + const userRegister: UserRegistration = { + userID: userId, + isPrivate: userTemplate.private, + charName: userTemplate.userName, + damage: damageName, + msgId: [msg.id, thread.id], + }; + await registerUser(userRegister, interaction, guildData); +} + +export async function findLocation( + userData: CharacterData, + interaction: Djs.CommandInteraction, + client: EClient, + ul: Translation, + charData: CharDataWithName, + user?: Djs.User | null +): Promise<{ + thread?: + | Djs.PrivateThreadChannel + | Djs.TextChannel + | Djs.NewsChannel + | Djs.PublicThreadChannel; + sheetLocation: PersonnageIds; +}> { + const sheetLocation: PersonnageIds = { + channelId: userData.messageId[1], + messageId: userData.messageId[0], + }; + const thread = await searchUserChannel( + client.settings, + interaction, + ul, + sheetLocation?.channelId + ); + if (!thread) { + await reply(interaction, { embeds: [embedError(ul("error.noThread"), ul)] }); + return { sheetLocation }; + } + const allowHidden = haveAccess(interaction, thread.id, user?.id ?? interaction.user.id); + if (!allowHidden && charData[user?.id ?? interaction.user.id]?.isPrivate) { + await reply(interaction, { embeds: [embedError(ul("error.private"), ul)] }); + return { sheetLocation }; + } + return { thread, sheetLocation }; +} + +/** + * Find a thread by their data or create it for roll + */ +export async function findThread( + db: Settings, + channel: Djs.TextChannel, + ul: Translation, + hidden?: string +) { + const guild = channel.guild.id; + const rollChannelId = !hidden ? db.get(guild, "rollChannel") : hidden; + if (rollChannelId) { + try { + const rollChannel = await channel.guild.channels.fetch(rollChannelId); + // noinspection SuspiciousTypeOfGuard + if ( + rollChannel instanceof Djs.ThreadChannel || + rollChannel instanceof Djs.TextChannel + ) { + return rollChannel; + } + } catch (e) { + let command = `${ul("config.name")} ${ul("changeThread.name")}`; + + if (hidden) { + db.delete(guild, "hiddenRoll"); + command = `${ul("config.name")} ${ul("hidden.title")}`; + } else db.delete(guild, "rollChannel"); + await sendLogs(ul("error.rollChannelNotFound", { command }), channel.guild, db); + } + } + await channel.threads.fetch(); + await channel.threads.fetchArchived(); + const mostRecentThread = channel.threads.cache.sort((a, b) => { + const aDate = a.createdTimestamp; + const bDate = b.createdTimestamp; + if (aDate && bDate) { + return bDate - aDate; + } + return 0; + }); + const threadName = `🎲 ${channel.name.replaceAll("-", " ")}`; + const thread = mostRecentThread.find( + (thread) => thread.name.startsWith("🎲") && !thread.archived + ); + if (thread) { + const threadThatMustBeArchived = mostRecentThread.filter( + (tr) => tr.name.startsWith("🎲") && !tr.archived && tr.id !== thread.id + ); + for (const thread of threadThatMustBeArchived) { + await thread[1].setArchived(true); + } + return thread; + } + if (mostRecentThread.find((thread) => thread.name === threadName && thread.archived)) { + const thread = mostRecentThread.find( + (thread) => thread.name === threadName && thread.archived + ); + if (thread) { + await thread.setArchived(false); + return thread; + } + } + //create thread + const newThread = await channel.threads.create({ + name: threadName, + reason: ul("roll.reason"), + }); + //delete the message about thread creation + await channel.lastMessage?.delete(); + return newThread; +} + +/** + * Find a forum channel already existing or creat it + */ +export async function findForumChannel( + forum: Djs.ForumChannel, + thread: Djs.ThreadChannel | Djs.TextChannel, + db: Settings, + ul: Translation, + hidden?: string +) { + const guild = forum.guild.id; + const rollChannelId = !hidden ? db.get(guild, "rollChannel") : hidden; + if (rollChannelId) { + try { + const rollChannel = await forum.guild.channels.fetch(rollChannelId); + if ( + rollChannel instanceof Djs.ThreadChannel || + rollChannel instanceof Djs.TextChannel + ) { + return rollChannel; + } + } catch (e) { + let command = `${ul("config.name")} ${ul("changeThread.name")}`; + + if (hidden) { + db.delete(guild, "hiddenRoll"); + command = `${ul("config.name")} ${ul("hidden.title")}`; + } else db.delete(guild, "rollChannel"); + await sendLogs(ul("error.rollChannelNotFound", { command }), forum.guild, db); + } + } + const allForumChannel = forum.threads.cache.sort((a, b) => { + const aDate = a.createdTimestamp; + const bDate = b.createdTimestamp; + if (aDate && bDate) { + return bDate - aDate; + } + return 0; + }); + const topic = thread.name; + const rollTopic = allForumChannel.find((thread) => thread.name === `🎲 ${topic}`); + const tags = await setTagsForRoll(forum); + if (rollTopic) { + //archive all other roll topic + if (rollTopic.archived) await rollTopic.setArchived(false); + await rollTopic.setAppliedTags([tags.id as string]); + return rollTopic; + } + //create new forum thread + return await forum.threads.create({ + name: `🎲 ${topic}`, + message: { content: ul("roll.reason") }, + appliedTags: [tags.id as string], + }); +} + +export async function threadToSend( + db: Settings, + channel: + | Djs.TextChannel + | Djs.PrivateThreadChannel + | Djs.NewsChannel + | Djs.StageChannel + | Djs.PublicThreadChannel + | Djs.VoiceChannel, + ul: Translation, + isHidden?: string +) { + const parentChannel = channel instanceof Djs.ThreadChannel ? channel.parent : channel; + return parentChannel instanceof Djs.TextChannel + ? await findThread(db, parentChannel, ul, isHidden) + : await findForumChannel( + channel.parent as Djs.ForumChannel, + channel as Djs.ThreadChannel, + db, + ul, + isHidden + ); +} diff --git a/src/utils/buttons.ts b/packages/bot/src/utils/button.ts similarity index 73% rename from src/utils/buttons.ts rename to packages/bot/src/utils/button.ts index 55fb3b78..bc0f6da2 100644 --- a/src/utils/buttons.ts +++ b/packages/bot/src/utils/button.ts @@ -1,5 +1,7 @@ -import type { Translation } from "@interfaces/discord"; +import type { Translation } from "@dicelette/types"; import * as Djs from "discord.js"; +import { ensureEmbed, reply } from "messages"; + /** * Button to edit the user embed character sheet * By default, only add the "add dice" button @@ -50,7 +52,7 @@ export function selectEditMenu(ul: Translation) { new Djs.StringSelectMenuOptionBuilder() .setLabel(ul("button.avatar.title")) .setValue("avatar") - .setEmoji("🖼️") + .setEmoji("🖼") .setDescription(ul("button.avatar.description")), new Djs.StringSelectMenuOptionBuilder() .setLabel(ul("common.user")) @@ -61,6 +63,31 @@ export function selectEditMenu(ul: Translation) { return new Djs.ActionRowBuilder().addComponents(select); } +/** + * Interaction when the cancel button is pressed + * Also prevent to cancel by user not authorized + * @param interaction {Djs.ButtonInteraction} + * @param ul {Translation} + * @param interactionUser {User} + */ +export async function cancel( + interaction: Djs.ButtonInteraction, + ul: Translation, + interactionUser: Djs.User +) { + const embed = ensureEmbed(interaction.message); + const user = + embed.fields + .find((field) => field.name === ul("common.user")) + ?.value.replace("<@", "") + .replace(">", "") === interactionUser.id; + const isModerator = interaction.guild?.members.cache + .get(interactionUser.id) + ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); + if (user || isModerator) await interaction.message.delete(); + else await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); +} + /** * Add the cancel and continue button when registering user and their are multiple page * @param ul {Translation} @@ -79,27 +106,3 @@ export function continueCancelButtons(ul: Translation) { cancelButton, ]); } - -/** - * Button when registering the user, adding the "add dice" button - * @param ul {Translation} - */ -export function registerDmgButton(ul: Translation) { - const validateButton = new Djs.ButtonBuilder() - .setCustomId("validate") - .setLabel(ul("button.validate")) - .setStyle(Djs.ButtonStyle.Success); - const cancelButton = new Djs.ButtonBuilder() - .setCustomId("cancel") - .setLabel(ul("button.cancel")) - .setStyle(Djs.ButtonStyle.Danger); - const registerDmgButton = new Djs.ButtonBuilder() - .setCustomId("add_dice_first") - .setLabel(ul("button.dice")) - .setStyle(Djs.ButtonStyle.Primary); - return new Djs.ActionRowBuilder().addComponents([ - registerDmgButton, - validateButton, - cancelButton, - ]); -} diff --git a/packages/bot/src/utils/check.ts b/packages/bot/src/utils/check.ts new file mode 100644 index 00000000..468f1af3 --- /dev/null +++ b/packages/bot/src/utils/check.ts @@ -0,0 +1,74 @@ +import { findln, ln } from "@dicelette/localization"; +import type { Settings, UserData } from "@dicelette/types"; +import { logger } from "@dicelette/utils"; +import { verifyIfEmbedInDB } from "database"; +import * as Djs from "discord.js"; +import { embedError, ensureEmbed, reply } from "messages"; + +export async function allowEdit( + interaction: Djs.ButtonInteraction | Djs.StringSelectMenuInteraction, + db: Settings, + interactionUser: Djs.User +) { + const ul = ln(interaction.locale as Djs.Locale); + const embed = ensureEmbed(interaction.message); + const user = embed.fields + .find((field) => findln(field.name) === "common.user") + ?.value.replace("<@", "") + .replace(">", ""); + const isSameUser = user === interactionUser.id; + const isModerator = interaction.guild?.members.cache + .get(interactionUser.id) + ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); + const first = interaction.customId.includes("first"); + const userName = embed.fields.find((field) => + ["common.character", "common.charName"].includes(findln(field.name)) + ); + const userNameValue = + userName && findln(userName?.value) === "common.noSet" ? undefined : userName?.value; + if (!first && user) { + const { isInDb, coord } = verifyIfEmbedInDB( + db, + interaction.message, + user, + userNameValue + ); + if (!isInDb) { + const urlNew = `https://discord.com/channels/${interaction.guild!.id}/${coord?.channelId}/${coord?.messageId}`; + await reply(interaction, { + embeds: [embedError(ul("error.oldEmbed", { fiche: urlNew }), ul)], + ephemeral: true, + }); + //delete the message + try { + await interaction.message.delete(); + } catch (e) { + logger.warn("Error while deleting message", e, "allowEdit"); + } + return false; + } + } + if (isSameUser || isModerator) return true; + await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); + return false; +} + +export async function isUserNameOrId( + userId: string, + interaction: Djs.ModalSubmitInteraction +) { + if (!userId.match(/\d+/)) + return (await interaction.guild!.members.fetch({ query: userId })).first(); + return await interaction.guild!.members.fetch({ user: userId }); +} +export function serializeName( + userStatistique: UserData | undefined, + charName: string | undefined +) { + const serializedNameDB = userStatistique?.userName?.standardize(true); + const serializedNameQueries = charName?.standardize(true); + return ( + serializedNameDB !== serializedNameQueries || + (serializedNameQueries && serializedNameDB?.includes(serializedNameQueries)) + ); +} diff --git a/src/utils/import_csv.ts b/packages/bot/src/utils/import_csv.ts similarity index 97% rename from src/utils/import_csv.ts rename to packages/bot/src/utils/import_csv.ts index ed317512..ee960b75 100644 --- a/src/utils/import_csv.ts +++ b/packages/bot/src/utils/import_csv.ts @@ -1,10 +1,11 @@ import type { StatisticalTemplate } from "@dicelette/core"; -import type { UserData } from "@interfaces/database"; -import { ln } from "@localization/index"; -import { logger } from "@logger"; -import { InvalidCsvContent, InvalidURL, reply } from "@utils/index"; +import { ln } from "@dicelette/localization"; +import type { UserData } from "@dicelette/types"; +import { InvalidCsvContent, InvalidURL, logger } from "@dicelette/utils"; import * as Djs from "discord.js"; import Papa from "papaparse"; +import "uniformize"; +import { reply } from "messages"; export type CSVRow = { user: string; diff --git a/packages/bot/src/utils/index.ts b/packages/bot/src/utils/index.ts new file mode 100644 index 00000000..57c86456 --- /dev/null +++ b/packages/bot/src/utils/index.ts @@ -0,0 +1,7 @@ +//export all function from utils +export * from "./roll"; +export * from "./button"; +export * from "./check"; +export * from "./import_csv"; +export * from "./search"; +export * from "./roles"; diff --git a/packages/bot/src/utils/roles.ts b/packages/bot/src/utils/roles.ts new file mode 100644 index 00000000..4a92d035 --- /dev/null +++ b/packages/bot/src/utils/roles.ts @@ -0,0 +1,80 @@ +// noinspection SuspiciousTypeOfGuard + +import type { Settings } from "@dicelette/types"; +import { logger } from "@dicelette/utils"; +import * as Djs from "discord.js"; + +async function fetchDiceRole(diceEmbed: boolean, guild: Djs.Guild, role?: string) { + if (!diceEmbed || !role) return; + const diceRole = guild.roles.cache.get(role); + if (!diceRole) return await guild.roles.fetch(role); + return diceRole; +} + +async function fetchStatsRole(statsEmbed: boolean, guild: Djs.Guild, role?: string) { + if (!statsEmbed || !role) return; + const statsRole = guild.roles.cache.get(role); + if (!statsRole) return await guild.roles.fetch(role); + return statsRole; +} + +export function haveAccess( + interaction: Djs.BaseInteraction, + thread: Djs.GuildChannelResolvable, + user?: string +): boolean { + if (!user) return false; + if (user === interaction.user.id) return true; + //verify if the user have access to the channel/thread, like reading the channel + const member = interaction.guild?.members.cache.get(interaction.user.id); + if (!member || !thread) return false; + return ( + member.permissions.has(Djs.PermissionFlagsBits.ManageRoles) || + member.permissionsIn(thread).has(Djs.PermissionFlagsBits.ViewChannel) + ); +} + +export async function addAutoRole( + interaction: Djs.BaseInteraction, + member: string, + diceEmbed: boolean, + statsEmbed: boolean, + db: Settings +) { + const autoRole = db.get(interaction.guild!.id, "autoRole"); + if (!autoRole) return; + try { + let guildMember = interaction.guild!.members.cache.get(member); + if (!guildMember) { + //Use the fetch in case the member is not in the cache + guildMember = await interaction.guild!.members.fetch(member); + } + //fetch role + const diceRole = await fetchDiceRole(diceEmbed, interaction.guild!, autoRole.dice); + const statsRole = await fetchStatsRole( + statsEmbed, + interaction.guild!, + autoRole.stats + ); + + if (diceEmbed && diceRole) await guildMember.roles.add(diceRole); + + if (statsEmbed && statsRole) await guildMember.roles.add(statsRole); + } catch (e) { + logger.error("Error while adding role", e); + //delete the role from database so it will be skip next time + db.delete(interaction.guild!.id, "autoRole"); + const dbLogs = db.get(interaction.guild!.id, "logs"); + const errorMessage = `\`\`\`\n${(e as Error).message}\n\`\`\``; + if (dbLogs) { + const logs = await interaction.guild!.channels.fetch(dbLogs); + if (logs instanceof Djs.TextChannel) { + await logs.send(errorMessage); + } + } else { + //Dm the server owner because it's pretty important to know + const owner = await interaction.guild!.fetchOwner(); + await owner.send(errorMessage); + } + } +} diff --git a/src/utils/roll.ts b/packages/bot/src/utils/roll.ts similarity index 68% rename from src/utils/roll.ts rename to packages/bot/src/utils/roll.ts index 77ef9fdb..38b9cd7d 100644 --- a/src/utils/roll.ts +++ b/packages/bot/src/utils/roll.ts @@ -1,16 +1,26 @@ -import { ln, t } from "@localization"; - -import { generateStatsDice, replaceFormulaInDice, roll } from "@dicelette/core"; -import { DETECT_DICE_MESSAGE } from "@events/message_create"; -import type { UserData } from "@interfaces/database"; -import type { Settings, Translation } from "@interfaces/discord"; -import { logger } from "@logger"; -import type { EClient } from "@main"; - -import { deleteAfter, embedError, reply, timestamp } from "@utils"; -import { findForumChannel, findMessageBefore, findThread } from "@utils/find"; -import * as Djs from "discord.js"; -import { parseResult } from "../dice"; +import { + type CustomCritical, + generateStatsDice, + replaceFormulaInDice, +} from "@dicelette/core"; +import { ln, t } from "@dicelette/localization"; +import { + ResultAsText, + type Server, + convertCustomCriticalValue, + getRoll, +} from "@dicelette/parse_result"; +import type { Settings, Translation, UserData } from "@dicelette/types"; +import { logger } from "@dicelette/utils"; +import type { EClient } from "client"; +import type * as Djs from "discord.js"; +import { + deleteAfter, + embedError, + findMessageBefore, + reply, + threadToSend, +} from "messages"; /** * create the roll dice, parse interaction etc... When the slash-commands is used for dice @@ -24,7 +34,8 @@ export async function rollWithInteraction( user?: Djs.User, charName?: string, infoRoll?: { name: string; standardized: string }, - hideResult?: boolean | null + hideResult?: boolean | null, + customCritical?: { [name: string]: CustomCritical } | undefined ) { if (!channel || channel.isDMBased() || !channel.isTextBased() || !interaction.guild) return; @@ -33,43 +44,38 @@ export async function rollWithInteraction( interaction.guild.preferredLocale ?? interaction.locale; const ul = ln(langToUser); - const comments = dice.match(DETECT_DICE_MESSAGE)?.[3].replaceAll("*", "\\*"); - if (comments) { - //biome-ignore lint/style/noParameterAssign: We need to replace the dice with the message - dice = dice.replace(DETECT_DICE_MESSAGE, "$1"); - } - //biome-ignore lint/style/noParameterAssign: We need to replace the dice with the message - dice = dice.trim(); - const rollDice = roll(dice.trim().toLowerCase()); - if (!rollDice) { + const data: Server = { + lang: langToUser, + userId: user?.id ?? interaction.user.id, + config: db.get(interaction.guild.id), + }; + const result = getRoll(dice); + const defaultMsg = new ResultAsText( + result, + data, + critical, + charName, + infoRoll, + customCritical + ); + const output = defaultMsg.output; + if (defaultMsg.error) { await reply(interaction, { - embeds: [embedError(ul("error.invalidDice.withDice", { dice }), ul)], + embeds: [embedError(output, ul)], ephemeral: true, }); return; } - if (comments) { - rollDice.comment = comments; - rollDice.dice = `${dice} /* ${comments} */`; - } - const parser = parseResult(rollDice, ul, critical, !!infoRoll); - const userId = user?.id ?? interaction.user.id; - let mentionUser: string = Djs.userMention(userId); - const titleCharName = `__**${charName?.capitalize()}**__`; - mentionUser = charName ? `${titleCharName} (${mentionUser})` : mentionUser; - const infoRollTotal = (mention?: boolean, time?: boolean) => { - let user = " "; - if (mention) user = mentionUser; - else if (charName) user = titleCharName; - if (time) user += `${timestamp(db, interaction.guild!.id)}`; - if (user.trim().length > 0) user += `${ul("common.space")}:\n`; - if (infoRoll) return `${user}[__${infoRoll.name.capitalize()}__] `; - return user; - }; - const retrieveUser = infoRollTotal(true); + const disableThread = db.get(interaction.guild.id, "disableThread"); let rollChannel = db.get(interaction.guild.id, "rollChannel"); - const hideResultConfig = db.get(interaction.guild.id, "hiddenRoll"); + const hideResultConfig = db.get(interaction.guild.id, "hiddenRoll") as + | string + | boolean + | undefined; + logger.debug( + `hideResultConfig: ${hideResultConfig} - disable thread: ${disableThread}; rollChannel: ${rollChannel}` + ); const hidden = hideResult && hideResultConfig; let isHidden: undefined | string = undefined; if (hidden) { @@ -79,8 +85,8 @@ export async function rollWithInteraction( isHidden = hideResultConfig; } else if (typeof hideResultConfig === "boolean") { await reply(interaction, { - content: `${retrieveUser}${parser}`, - allowedMentions: { users: [userId] }, + content: output, + allowedMentions: { users: [data!.userId as string] }, ephemeral: true, }); return; @@ -88,51 +94,117 @@ export async function rollWithInteraction( } if (channel.name.startsWith("🎲") || disableThread || rollChannel === channel.id) { await reply(interaction, { - content: `${retrieveUser}${parser}`, - allowedMentions: { users: [userId] }, + content: output, + allowedMentions: { users: [data!.userId as string] }, ephemeral: !!hidden, }); return; } - const parentChannel = channel instanceof Djs.ThreadChannel ? channel.parent : channel; - const thread = - parentChannel instanceof Djs.TextChannel - ? await findThread(db, parentChannel, ul, isHidden) - : await findForumChannel( - channel.parent as Djs.ForumChannel, - channel as Djs.ThreadChannel, - db, - ul, - isHidden - ); + const thread = await threadToSend(db, channel, ul, isHidden); const rolLog = await thread.send("_ _"); - await rolLog.edit(`${infoRollTotal(true, true)}${parser}`); + const editMessage = defaultMsg.edit().result; + await rolLog.edit(editMessage); const rollLogEnabled = db.get(interaction.guild.id, "linkToLogs"); - const rolLogUrl = rollLogEnabled ? `\n\n-# ↪ ${rolLog.url}` : ""; + const rolLogUrl = rollLogEnabled ? rolLog.url : undefined; + const rollTextUrl = defaultMsg.logUrl(rolLogUrl).result; const inter = await reply(interaction, { - content: `${retrieveUser}${parser}${rolLogUrl}`, - allowedMentions: { users: [userId] }, + content: rollTextUrl, + allowedMentions: { users: [data!.userId as string] }, ephemeral: !!hidden, }); const anchor = db.get(interaction.guild.id, "context"); const dbTime = db.get(interaction.guild.id, "deleteAfter"); const timer = dbTime ? dbTime : 180000; - - let url = ""; + let messageId = undefined; if (anchor) { - url = `\n-# ↪ [${ul("common.context")}]()`; + messageId = inter.id; if (timer && timer > 0) { const messageBefore = await findMessageBefore(channel, inter, interaction.client); - if (messageBefore) - url = `\n\n-# ↪ [${ul("common.context")}]()`; + if (messageBefore) messageId = messageBefore.id; } - await rolLog.edit(`${infoRollTotal(true, true)}${parser}${url}`); + const res = defaultMsg.context({ + guildId: interaction.guild.id, + channelId: channel.id, + messageId, + }).result; + await rolLog.edit(res); } if (!disableThread) await deleteAfter(inter, timer); return; } +export async function rollDice( + interaction: Djs.CommandInteraction, + client: EClient, + userStatistique: UserData, + options: Djs.CommandInteractionOptionResolver, + ul: Translation, + charOptions?: string, + user?: Djs.User, + hideResult?: boolean | null +) { + let atq = options.getString(t("rAtq.atq_name.name"), true); + const infoRoll = { + name: atq, + standardized: atq.standardize(), + }; + atq = atq.standardize(); + const comments = options.getString(t("dbRoll.options.comments.name")) ?? ""; + //search dice + let dice = userStatistique.damage?.[atq]; + // noinspection LoopStatementThatDoesntLoopJS + while (!dice) { + const userData = client.settings + .get(interaction.guild!.id, `user.${user?.id ?? interaction.user.id}`) + ?.find((char) => { + return char.charName?.subText(charOptions); + }); + const damageName = userData?.damageName ?? []; + const findAtqInList = damageName.find((atqName) => atqName.subText(atq)); + if (findAtqInList) { + atq = findAtqInList; + dice = userStatistique.damage?.[findAtqInList]; + } + if (dice) break; + await reply(interaction, { + embeds: [ + embedError( + ul("error.noDamage", { + atq: infoRoll.name.capitalize(), + charName: charOptions ?? "", + }), + ul + ), + ], + ephemeral: true, + }); + return; + } + dice = generateStatsDice(dice, userStatistique.stats); + const modificator = options.getNumber(t("dbRoll.options.modificator.name")) ?? 0; + const modificatorString = + modificator > 0 ? `+${modificator}` : modificator < 0 ? `${modificator}` : ""; + const comparatorMatch = /(?[><=!]+)(?(\d+))/.exec(dice); + let comparator = ""; + if (comparatorMatch) { + dice = dice.replace(comparatorMatch[0], ""); + comparator = comparatorMatch[0]; + } + const roll = `${dice.trimAll()}${modificatorString}${comparator} ${comments}`; + await rollWithInteraction( + interaction, + roll, + interaction.channel as Djs.TextBasedChannel, + client.settings, + undefined, + user, + charOptions, + infoRoll, + hideResult + ); +} + export async function rollStatistique( interaction: Djs.CommandInteraction, client: EClient, @@ -151,6 +223,7 @@ export async function rollStatistique( const modification = options.getNumber(t("dbRoll.options.modificator.name")) ?? 0; let userStat = userStatistique.stats?.[standardizedStatistic]; + // noinspection LoopStatementThatDoesntLoopJS while (!userStat) { const guildData = client.settings.get(interaction.guild!.id, "templateID.statsName"); if (userStatistique.stats && guildData) { @@ -197,6 +270,9 @@ export async function rollStatistique( comparator = comparatorMatch[0]; } const roll = `${replaceFormulaInDice(dice).trimAll()}${modificationString}${comparator} ${comments}`; + const customCritical = template.customCritical + ? convertCustomCriticalValue(template.customCritical, userStat) + : undefined; await rollWithInteraction( interaction, roll, @@ -206,77 +282,7 @@ export async function rollStatistique( user, optionChar, { name: statistic, standardized: standardizedStatistic }, - hideResult - ); -} - -export async function rollDice( - interaction: Djs.CommandInteraction, - client: EClient, - userStatistique: UserData, - options: Djs.CommandInteractionOptionResolver, - ul: Translation, - charOptions?: string, - user?: Djs.User, - hideResult?: boolean | null -) { - let atq = options.getString(t("rAtq.atq_name.name"), true); - const infoRoll = { - name: atq, - standardized: atq.standardize(), - }; - atq = atq.standardize(); - const comments = options.getString(t("dbRoll.options.comments.name")) ?? ""; - //search dice - let dice = userStatistique.damage?.[atq]; - while (!dice) { - const userData = client.settings - .get(interaction.guild!.id, `user.${user?.id ?? interaction.user.id}`) - ?.find((char) => { - return char.charName?.subText(charOptions); - }); - const damageName = userData?.damageName ?? []; - const findAtqInList = damageName.find((atqName) => atqName.subText(atq)); - if (findAtqInList) { - atq = findAtqInList; - dice = userStatistique.damage?.[findAtqInList]; - } - if (dice) break; - await reply(interaction, { - embeds: [ - embedError( - ul("error.noDamage", { - atq: infoRoll.name.capitalize(), - charName: charOptions ?? "", - }), - ul - ), - ], - ephemeral: true, - }); - return; - } - dice = generateStatsDice(dice, userStatistique.stats); - const modificator = options.getNumber(t("dbRoll.options.modificator.name")) ?? 0; - const modificatorString = - modificator > 0 ? `+${modificator}` : modificator < 0 ? `${modificator}` : ""; - const comparatorMatch = /(?[><=!]+)(?(\d+))/.exec(dice); - let comparator = ""; - if (comparatorMatch) { - dice = dice.replace(comparatorMatch[0], ""); - comparator = comparatorMatch[0]; - } - const roll = `${dice.trimAll()}${modificatorString}${comparator} ${comments}`; - logger.debug(dice.trimAll()); - await rollWithInteraction( - interaction, - roll, - interaction.channel as Djs.TextBasedChannel, - client.settings, - undefined, - user, - charOptions, - infoRoll, - hideResult + hideResult, + customCritical ); } diff --git a/packages/bot/src/utils/search.ts b/packages/bot/src/utils/search.ts new file mode 100644 index 00000000..df7b80ad --- /dev/null +++ b/packages/bot/src/utils/search.ts @@ -0,0 +1,58 @@ +// noinspection SuspiciousTypeOfGuard + +import type { DiscordChannel, Settings, Translation } from "@dicelette/types"; +import * as Djs from "discord.js"; +import { embedError, reply, sendLogs } from "messages"; +export async function searchUserChannel( + guildData: Settings, + interaction: Djs.BaseInteraction, + ul: Translation, + channelId: string, + register?: boolean +): Promise { + let thread: Djs.TextChannel | Djs.AnyThreadChannel | undefined | Djs.GuildBasedChannel = + undefined; + try { + const channel = await interaction.guild?.channels.fetch(channelId); + if (channel instanceof Djs.ForumChannel && register) return; + if ( + !channel || + channel instanceof Djs.CategoryChannel || + channel instanceof Djs.ForumChannel || + channel instanceof Djs.MediaChannel || + channel instanceof Djs.StageChannel || + channel instanceof Djs.VoiceChannel + ) { + if ( + interaction instanceof Djs.CommandInteraction || + interaction instanceof Djs.ButtonInteraction || + interaction instanceof Djs.ModalSubmitInteraction + ) + await interaction?.channel?.send({ + embeds: [embedError(ul("error.noThread"), ul)], + }); + + await sendLogs(ul("error.noThread"), interaction.guild as Djs.Guild, guildData); + return; + } + thread = channel; + } catch (error) { + console.error("Error while fetching channel", error); + return; + } + if (!thread) { + if ( + interaction instanceof Djs.CommandInteraction || + interaction instanceof Djs.ButtonInteraction || + interaction instanceof Djs.ModalSubmitInteraction + ) { + if (interaction.replied) + await interaction.editReply({ embeds: [embedError(ul("error.noThread"), ul)] }); + else await reply(interaction, { embeds: [embedError(ul("error.noThread"), ul)] }); + } else + await sendLogs(ul("error.noThread"), interaction.guild as Djs.Guild, guildData); + return; + } + if (thread.isThread() && thread.archived) thread.setArchived(false); + return thread; +} diff --git a/packages/bot/tsconfig.json b/packages/bot/tsconfig.json new file mode 100644 index 00000000..63787c73 --- /dev/null +++ b/packages/bot/tsconfig.json @@ -0,0 +1,26 @@ +{ + "ts-node": { + "require": ["tsconfig-paths/register"], + "esm": true + }, + "tsc-alias": { + "resolveFullPaths": true + }, + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "paths": { + "client": ["packages/bot/src/client.js"], + "messages": ["packages/bot/src/messages/index.js"], + "utils": ["packages/bot/src/utils/index.js"], + "database": ["packages/bot/src/database/index.js"], + "features": ["packages/bot/src/features/index.js"], + "commands": ["packages/bot/src/commands/index.js"], + "event": ["packages/bot/src/events/index.js"], + "locales": ["packages/bot/src/locales.js"] + } + }, + "include": ["src", "index.ts", "package.json"], + "exclude": ["./dist"] +} diff --git a/packages/localization/index.ts b/packages/localization/index.ts new file mode 100644 index 00000000..50b2d114 --- /dev/null +++ b/packages/localization/index.ts @@ -0,0 +1,12 @@ +import { resources } from "./src/types"; + +export * from "./src/types"; +export * from "./src/translate"; +export * from "./src/flattenJson"; +import i18next from "i18next"; +await i18next.init({ + lng: "en", + fallbackLng: "en", + returnNull: false, + resources, +}); diff --git a/src/localizations/locales/en.json b/packages/localization/locales/en.json similarity index 100% rename from src/localizations/locales/en.json rename to packages/localization/locales/en.json diff --git a/src/localizations/locales/fr.json b/packages/localization/locales/fr.json similarity index 100% rename from src/localizations/locales/fr.json rename to packages/localization/locales/fr.json diff --git a/packages/localization/package.json b/packages/localization/package.json new file mode 100644 index 00000000..3f681a6d --- /dev/null +++ b/packages/localization/package.json @@ -0,0 +1,19 @@ +{ + "name": "@dicelette/localization", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "dist/index.js", + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/Mara-Li/discord-dicelette.git" + }, + "private": true, + "dependencies": { + "@dicelette/utils": "workspace:*" + } +} diff --git a/packages/localization/src/flattenJson.ts b/packages/localization/src/flattenJson.ts new file mode 100644 index 00000000..3b8f530f --- /dev/null +++ b/packages/localization/src/flattenJson.ts @@ -0,0 +1,27 @@ +import { resources } from "./types"; + +interface JsonObject { + //biome-ignore lint/suspicious/noExplicitAny: + [key: string]: any; +} + +export function flattenJson( + obj: JsonObject, + parentKey = "", + result: JsonObject = {} +): JsonObject { + for (const key in obj) { + // biome-ignore lint/suspicious/noPrototypeBuiltins: + if (obj.hasOwnProperty(key)) { + const newKey = parentKey ? `${parentKey}.${key}` : key; + if (typeof obj[key] === "object" && !Array.isArray(obj[key])) { + flattenJson(obj[key], newKey, result); + } else { + result[newKey] = obj[key]; + } + } + } + return result; +} + +export const ALL_TRANSLATION_KEYS = Object.keys(flattenJson(resources.en.translation)); diff --git a/src/localizations/index.ts b/packages/localization/src/translate.ts similarity index 71% rename from src/localizations/index.ts rename to packages/localization/src/translate.ts index 7c732b2b..dec5eac8 100644 --- a/src/localizations/index.ts +++ b/packages/localization/src/translate.ts @@ -1,17 +1,16 @@ -import * as Djs from "discord.js"; -import { default as i18next } from "i18next"; - import { DiceTypeError, EmptyObjectError, FormulaError, NoStatisticsError, } from "@dicelette/core"; -import { logger } from "@logger"; -import { InvalidCsvContent, NoChannel, NoEmbed } from "@utils"; -import { resources } from "./init"; +import { InvalidCsvContent, NoChannel, NoEmbed, logger } from "@dicelette/utils"; +import * as Djs from "discord.js"; +import { default as i18next } from "i18next"; +import { ALL_TRANSLATION_KEYS } from "./flattenJson"; +import { resources } from "./types"; -export const ALL_TRANSLATION_KEYS = Object.keys(flattenJson(resources.en.translation)); +export const t = i18next.getFixedT("en"); export function ln(userLang: Djs.Locale) { if (userLang === Djs.Locale.EnglishUS || userLang === Djs.Locale.EnglishGB) @@ -22,8 +21,6 @@ export function ln(userLang: Djs.Locale) { return i18next.getFixedT(localeName?.[1] ?? "en"); } -export const t = i18next.getFixedT("en"); - export function lError( e: Error, interaction?: Djs.BaseInteraction, @@ -70,19 +67,6 @@ export function lError( return ul("error.generic.withWarning", { e }); } -/** - * Create an object with all the translations for a specific key - * @example - * ```ts - * cmdLn("hello"): - * { - * "en": "Hello", - * "fr": "Bonjour" - * } - * ``` - * @param key i18n key - * @returns - */ export function cmdLn(key: string) { const localized: Djs.LocalizationMap = {}; const allValidLocale = Object.entries(Djs.Locale); @@ -98,34 +82,6 @@ export function cmdLn(key: string) { return localized; } -interface JsonObject { - //biome-ignore lint/suspicious/noExplicitAny: - [key: string]: any; -} - -export function flattenJson( - obj: JsonObject, - parentKey = "", - result: JsonObject = {} -): JsonObject { - for (const key in obj) { - // biome-ignore lint/suspicious/noPrototypeBuiltins: - if (obj.hasOwnProperty(key)) { - const newKey = parentKey ? `${parentKey}.${key}` : key; - if (typeof obj[key] === "object" && !Array.isArray(obj[key])) { - flattenJson(obj[key], newKey, result); - } else { - result[newKey] = obj[key]; - } - } - } - return result; -} - -/** - * Get the translation key from the translation text - * @example : "Nom du personnage" => ""common.charName" - */ export function findln(translatedText: string) { const allLocales = Object.keys(resources); for (const locale of allLocales) { diff --git a/packages/localization/src/types.ts b/packages/localization/src/types.ts new file mode 100644 index 00000000..d0eac8f5 --- /dev/null +++ b/packages/localization/src/types.ts @@ -0,0 +1,17 @@ +import EnglishUS from "../locales/en.json" assert { type: "json" }; +import French from "../locales/fr.json" assert { type: "json" }; + +export const resources = { + en: { + translation: EnglishUS, + }, + fr: { + translation: French, + }, +}; + +export enum LocalePrimary { + // noinspection JSUnusedGlobalSymbols + French = "Français", + English = "English", +} diff --git a/packages/localization/tsconfig.json b/packages/localization/tsconfig.json new file mode 100644 index 00000000..755947bb --- /dev/null +++ b/packages/localization/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./" + }, + "include": ["./src/**/*", "locales/**/*.json", "index.ts"], + "exclude": ["./dist"] +} diff --git a/packages/parse_result/index.ts b/packages/parse_result/index.ts new file mode 100644 index 00000000..975e7879 --- /dev/null +++ b/packages/parse_result/index.ts @@ -0,0 +1,5 @@ +export * from "./src/check"; +export * from "./src/result_as_text"; +export * from "./src/interfaces"; +export * from "./src/utils"; +export * from "./src/convert_embed"; diff --git a/packages/parse_result/package.json b/packages/parse_result/package.json new file mode 100644 index 00000000..a9837cc6 --- /dev/null +++ b/packages/parse_result/package.json @@ -0,0 +1,33 @@ +{ + "name": "@dicelette/parse_result", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "type": "module", + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json", + "test": "vitest" + }, + "repository": { + "type": "git", + "url": "https://github.com/Dicelette/discord-dicelette.git" + }, + "private": true, + "dependencies": { + "@dicelette/localization": "workspace:*", + "@dicelette/types": "workspace:*", + "@dicelette/utils": "workspace:*", + "mathjs": "^14.0.0", + "run": "^1.5.0", + "test": "^3.3.0", + "vite-tsconfig-paths": "^5.1.3", + "vitest": "^2.1.8" + }, + "devDependencies": { + "@types/bun": "^1.1.14", + "@types/papaparse": "^5.3.15", + "moment": "^2.30.1", + "papaparse": "^5.4.1" + } +} diff --git a/packages/parse_result/src/check.ts b/packages/parse_result/src/check.ts new file mode 100644 index 00000000..205d122f --- /dev/null +++ b/packages/parse_result/src/check.ts @@ -0,0 +1,23 @@ +import { type Resultat, roll } from "@dicelette/core"; +import { DETECT_DICE_MESSAGE } from "./interfaces.js"; + +export function isRolling(content: string) { + const detectRoll = content.match(/\[(.*)\]/)?.[1]; + const comments = content.match(DETECT_DICE_MESSAGE)?.[3].replaceAll("*", "\\*"); + if (comments && !detectRoll) { + const diceValue = content.match(/^\S*#?d\S+|\{.*\}/i); + if (!diceValue) return; + content = content.replace(DETECT_DICE_MESSAGE, "$1"); + } + let result: Resultat | undefined; + try { + result = detectRoll ? roll(detectRoll.toLowerCase()) : roll(content.toLowerCase()); + } catch (e) { + return undefined; + } + if (comments && !detectRoll && result) { + result.dice = `${result.dice} /* ${comments} */`; + result.comment = comments; + } + return { result, detectRoll }; +} diff --git a/packages/parse_result/src/convert_embed.ts b/packages/parse_result/src/convert_embed.ts new file mode 100644 index 00000000..a695832a --- /dev/null +++ b/packages/parse_result/src/convert_embed.ts @@ -0,0 +1,58 @@ +import type { Critical, CustomCritical } from "@dicelette/core"; +import { parseCustomCritical } from "./result_as_text"; + +export function parseEmbedToCritical(embed: { [name: string]: string }): { + [name: string]: CustomCritical; +} { + const customCritical: { [name: string]: CustomCritical } = {}; + //remove the 3 first field from the embed + embed["roll.critical.success"] = ""; + embed["roll.critical.failure"] = ""; + embed["common.dice"] = ""; + for (const [name, value] of Object.entries(embed)) { + if (value.length === 0) continue; + const custom = parseCustomCritical(name, value); + if (custom) { + Object.assign(customCritical, custom); + } + } + return customCritical; +} + +export function parseEmbedToStats( + embed?: { [name: string]: string }, + integrateCombinaison = true +) { + let stats: { [name: string]: number } | undefined = undefined; + if (embed) { + stats = {}; + for (const [name, damageValue] of Object.entries(embed)) { + const value = Number.parseInt(damageValue.removeBacktick(), 10); + if (Number.isNaN(value)) { + //it's a combinaison + //remove the `x` = text; + const combinaison = damageValue.split("=")[1].trim(); + if (integrateCombinaison) + stats[name.unidecode()] = Number.parseInt(combinaison, 10); + } else stats[name.unidecode()] = value; + } + } + return stats; +} + +export function parseTemplateField(embed: { [name: string]: string }): { + diceType?: string; + critical?: Critical; + customCritical?: { + [name: string]: CustomCritical; + }; +} { + return { + diceType: embed?.["common.dice"] || undefined, + critical: { + success: Number.parseInt(embed?.["roll.critical.success"], 10), + failure: Number.parseInt(embed?.["roll.critical.failure"], 10), + }, + customCritical: parseEmbedToCritical(embed), + }; +} diff --git a/packages/parse_result/src/interfaces.ts b/packages/parse_result/src/interfaces.ts new file mode 100644 index 00000000..d1182596 --- /dev/null +++ b/packages/parse_result/src/interfaces.ts @@ -0,0 +1,14 @@ +import type { GuildData } from "@dicelette/types"; +import type * as Djs from "discord.js"; +export const DETECT_DICE_MESSAGE = /([\w\.]+|(\{.*\})) (.*)/i; + +export interface Server { + lang: Djs.Locale; + userId?: string; + config?: Partial; +} + +export type RollResult = { + error?: boolean; + result: string; +}; diff --git a/packages/parse_result/src/result_as_text.ts b/packages/parse_result/src/result_as_text.ts new file mode 100644 index 00000000..67ecb525 --- /dev/null +++ b/packages/parse_result/src/result_as_text.ts @@ -0,0 +1,327 @@ +import { + COMMENT_REGEX, + type Compare, + type CustomCritical, + type Resultat, + roll, +} from "@dicelette/core"; +import type { Translation } from "@dicelette/types"; +import { evaluate } from "mathjs"; +import { DETECT_DICE_MESSAGE, type Server } from "./interfaces"; +import { timestamp } from "./utils.js"; +import "uniformize"; +import { ln } from "@dicelette/localization"; + +export class ResultAsText { + parser?: string; + error?: boolean; + output: string; + private readonly data: Server; + private readonly ul: Translation; + private readonly charName?: string; + private readonly infoRoll?: { name: string; standardized: string }; + private readonly resultat?: Resultat; + constructor( + result: Resultat | undefined, + data: Server, + critical?: { failure?: number; success?: number }, + charName?: string, + infoRoll?: { name: string; standardized: string }, + customCritical?: { [name: string]: CustomCritical } + ) { + this.data = data; + this.infoRoll = infoRoll; + this.ul = ln(data.lang); + this.resultat = result; + let parser = ""; + if (!result) { + this.error = true; + this.output = this.ul("roll.error"); + } else { + parser = this.parseResult(!!infoRoll, critical, customCritical); + } + this.output = this.defaultMessage(); + this.parser = parser; + this.charName = charName; + } + + defaultMessage() { + return !this.error + ? `${this.infoRollTotal(true)}${this.parser}` + : this.ul("roll.error"); + } + + private infoRollTotal(mention?: boolean, time?: boolean) { + let mentionUser = `<@${this.data.userId}>`; + const titleCharName = `__**${this.charName?.capitalize()}**__`; + mentionUser = this.charName ? `${titleCharName} (${mentionUser})` : mentionUser; + let user = " "; + if (mention) user = mentionUser; + else if (this.charName) user = titleCharName; + if (time) user += `${timestamp(this.data?.config?.timestamp)}`; + if (user.trim().length > 0) user += `${this.ul("common.space")}:\n`; + if (this.infoRoll) return `${user}[__${this.infoRoll.name.capitalize()}__] `; + return user; + } + edit() { + return { result: `${this.infoRollTotal(true, true)}${this.parser}` }; + } + logUrl(url?: string) { + return { + result: `${this.infoRollTotal(true, true)}${this.parser}${this.createUrl(undefined, url)}`, + }; + } + context(context: { guildId: string; channelId: string; messageId: string }) { + return { + result: `${this.infoRollTotal(true, true)}${this.parser}${this.createUrl(context)}`, + }; + } + + createUrl( + context?: { guildId: string; channelId: string; messageId: string }, + logUrl?: string + ) { + if (logUrl) return `\n\n-# ↪ ${logUrl}`; + if (!context) return ""; + const { guildId, channelId, messageId } = context; + return `\n\n-# ↪ [${this.ul("common.context")}]()`; + } + + private parseResult( + interaction?: boolean, + critical?: { failure?: number; success?: number }, + customCritical?: { [name: string]: CustomCritical } + ) { + if (!this.resultat) return ""; + const regexForFormulesDices = /^[✕◈✓]/; + let msgSuccess: string; + const messageResult = this.resultat.result.split(";"); + let successOrFailure = ""; + let isCritical: undefined | "failure" | "success" | "custom" = undefined; + if (this.resultat.compare) { + msgSuccess = ""; + let total = 0; + const natural: number[] = []; + for (const r of messageResult) { + if (r.match(regexForFormulesDices)) { + msgSuccess += `${r + .replaceAll(";", "\n") + .replaceAll(":", " ⟶") + .replaceAll(/ = (\S+)/g, " = ` $1 `") + .replaceAll("*", "\\*")}\n`; + continue; + } + const tot = r.match(/ = (\d+)/); + if (tot) { + total = Number.parseInt(tot[1], 10); + } + + successOrFailure = evaluate( + `${total} ${this.resultat.compare.sign} ${this.resultat.compare.value}` + ) + ? `**${this.ul("roll.success")}**` + : `**${this.ul("roll.failure")}**`; + // noinspection RegExpRedundantEscape + const naturalDice = r.matchAll(/\[(\d+)\]/gi); + for (const dice of naturalDice) { + natural.push(Number.parseInt(dice[1], 10)); + } + if (critical) { + if (critical.failure && natural.includes(critical.failure)) { + successOrFailure = `**${this.ul("roll.critical.failure")}**`; + isCritical = "failure"; + } else if (critical.success && natural.includes(critical.success)) { + successOrFailure = `**${this.ul("roll.critical.success")}**`; + isCritical = "success"; + } + } + if (customCritical) { + for (const [name, custom] of Object.entries(customCritical)) { + const valueToCompare = custom.onNaturalDice ? natural : total; + const success = evaluate(`${valueToCompare} ${custom.sign} ${custom.value}`); + console.debug(success); + if (success) { + successOrFailure = `**${name}**`; + isCritical = "custom"; + break; + } + } + } + const totalSuccess = this.resultat.compare + ? ` = \`${total} ${this.goodCompareSign(this.resultat.compare, total)} [${this.resultat.compare.value}]\`` + : `= \`${total}\``; + msgSuccess += `${successOrFailure} — ${r + .replaceAll(":", " ⟶") + .replaceAll(/ = (\S+)/g, totalSuccess) + .replaceAll("*", "\\*")}\n`; + total = 0; + } + } else + msgSuccess = `${this.resultat.result + .replaceAll(";", "\n") + .replaceAll(":", " ⟶") + .replaceAll(/ = (\S+)/g, " = ` $1 `") + .replaceAll("*", "\\*")}`; + const comment = this.resultat.comment + ? `*${this.resultat.comment + .replaceAll(/(\\\*|#|\*\/|\/\*)/g, "") + .replaceAll("×", "*") + .trim()}*\n` + : interaction + ? "\n" + : ""; + const dicesResult = /(?\S+) ⟶ (?.*) =/; + const splitted = msgSuccess.split("\n"); + const finalRes = []; + for (let res of splitted) { + const matches = dicesResult.exec(res); + if (matches) { + const { entry, calc } = matches.groups || {}; + if (entry) { + const entryStr = entry.replaceAll("\\*", "×"); + res = res.replace(entry, `\`${entryStr}\``); + } + if (calc) { + const calcStr = calc.replaceAll("\\*", "×"); + res = res.replace(calc, `\`${calcStr}\``); + } + } + if (isCritical === "failure") { + res = res.replace( + regexForFormulesDices, + `**${this.ul("roll.critical.failure")}** —` + ); + } else if (isCritical === "success") { + res = res.replace( + regexForFormulesDices, + `**${this.ul("roll.critical.success")}** —` + ); + } else if (isCritical === "custom") { + res = res.replace(regexForFormulesDices, `${successOrFailure} —`); + } else { + res = res + .replace("✕", `**${this.ul("roll.failure")}** —`) + .replace("✓", `**${this.ul("roll.success")}** —`); + } + + finalRes.push(res.trimStart()); + } + return `${comment} ${finalRes.join("\n ").trimEnd()}`; + } + + /** + * Replace the compare sign as it will invert the result for a better reading + * As the comparaison is after the total (like 20>10) + * @param {Compare} compare + * @param {number} total + */ + private goodCompareSign( + compare: Compare, + total: number + ): "<" | ">" | "≥" | "≤" | "=" | "!=" | "==" | "" { + //as the comparaison value is AFTER the total, we need to invert the sign to have a good comparaison string + const { sign, value } = compare; + const success = evaluate(`${total} ${sign} ${value}`); + if (success) { + return sign.replace(">=", "≥").replace("<=", "≤") as + | "<" + | ">" + | "≥" + | "≤" + | "=" + | "" + | "!=" + | "=="; + } + switch (sign) { + case "<": + return ">"; + case ">": + return "<"; + case ">=": + return "≤"; + case "<=": + return "≥"; + case "=": + return "="; + case "!=": + return "!="; + case "==": + return "=="; + default: + return ""; + } + } +} + +export function getRoll(dice: string): Resultat | undefined { + const comments = dice.match(DETECT_DICE_MESSAGE)?.[3].replaceAll("*", "\\*"); + if (comments) { + dice = dice.replace(DETECT_DICE_MESSAGE, "$1"); + } + dice = dice.trim(); + const rollDice = roll(dice.trim().toLowerCase()); + if (!rollDice) { + return undefined; + } + if (comments) { + rollDice.comment = comments; + rollDice.dice = `${dice} /* ${comments} */`; + } + return rollDice; +} + +export function rollContent( + result: Resultat, + parser: string, + linkToOriginal: string, + authorId?: string, + time?: boolean, + reply?: boolean +) { + if (reply) return `${parser}${linkToOriginal}`; + const signMessage = result.compare + ? `${result.compare.sign} ${result.compare.value}` + : ""; + const authorMention = `*<@${authorId}>* (🎲 \`${result.dice.replace(COMMENT_REGEX, "")}${signMessage ? ` ${signMessage}` : ""}\`)`; + return `${authorMention}${timestamp(time)}\n${parser}${linkToOriginal}`; +} + +/** + * A function that turn `(N) Name SIGN VALUE` into the custom critical object as `{[name]: CustomCritical}` + */ +export function parseCustomCritical( + name: string, + customCritical: string +): { [name: string]: CustomCritical } | undefined { + const findPart = /(?([<>=!]+))(?.*)/gi; + const match = findPart.exec(customCritical); + if (!match) return; + const { sign, value } = match.groups || {}; + if (!name || !sign || !value) return; + const onNaturalDice = name.startsWith("(N)"); + const nameStr = onNaturalDice ? name.replace("(N)", "") : name; + return { + [nameStr.trimAll()]: { + sign: sign.trimAll() as "<" | ">" | "<=" | ">=" | "!=" | "==", + value: value.trimAll(), + onNaturalDice, + }, + }; +} + +export function convertCustomCriticalValue( + custom: { [name: string]: CustomCritical }, + stats: number +) { + const customCritical: { [name: string]: CustomCritical } = {}; + for (const [name, value] of Object.entries(custom)) { + const newValue = value.value.replace("$", stats.toString()); + customCritical[name] = { + onNaturalDice: value.onNaturalDice, + sign: value.sign, + value: evaluate(newValue.replaceAll("{{", "").replaceAll("}}", "")).toString(), + }; + } + return customCritical; +} diff --git a/packages/parse_result/src/utils.ts b/packages/parse_result/src/utils.ts new file mode 100644 index 00000000..53b59574 --- /dev/null +++ b/packages/parse_result/src/utils.ts @@ -0,0 +1,6 @@ +import moment from "moment"; + +export function timestamp(time?: boolean) { + if (time) return ` • -`; + return ""; +} diff --git a/packages/parse_result/tests/rollText.test.ts b/packages/parse_result/tests/rollText.test.ts new file mode 100644 index 00000000..8d8ad856 --- /dev/null +++ b/packages/parse_result/tests/rollText.test.ts @@ -0,0 +1,52 @@ +import type { CustomCritical, Resultat } from "@dicelette/core"; +import { ln } from "@dicelette/localization"; +import * as Djs from "discord.js"; +import { describe, expect, it } from "vitest"; +import type { Server } from "../src/interfaces"; +import { ResultAsText, convertCustomCriticalValue, getRoll } from "../src/result_as_text"; +const data: Server = { + lang: Djs.Locale.EnglishUS, + userId: "mara__li", +}; + +describe("roll", () => { + it("simple roll with 1d20", () => { + const rollIng = getRoll("1d20"); + expect(rollIng?.total).toBeLessThanOrEqual(20); + }); +}); + +describe("custom critical roll", () => { + const customCritical: CustomCritical = { + onNaturalDice: false, + value: "round($/2)", + sign: ">", + }; + it("replace the value with the stats", () => { + const result = convertCustomCriticalValue({ test: customCritical }, 6); + expect(result.test.value).toBe("3"); + }); + it("should display the name of critical roll", () => { + const result: Resultat = { + dice: "1d20", + compare: { + sign: ">", + value: 3, + }, + result: "1d20: [4] = 4", + total: 4, + }; + const critical = convertCustomCriticalValue({ test: customCritical }, 6); + const ul = ln(data.lang); + const res = new ResultAsText( + result, + data, + { success: 20, failure: 1 }, + undefined, + undefined, + critical + ); + const text = res.defaultMessage(); + expect(text).toContain("test"); + }); +}); diff --git a/packages/parse_result/tsconfig.json b/packages/parse_result/tsconfig.json new file mode 100644 index 00000000..aaecd6c7 --- /dev/null +++ b/packages/parse_result/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./" + }, + "include": ["src", "index.ts"], + "exclude": ["./dist"] +} diff --git a/packages/parse_result/vitest.config.ts b/packages/parse_result/vitest.config.ts new file mode 100644 index 00000000..ecc213e0 --- /dev/null +++ b/packages/parse_result/vitest.config.ts @@ -0,0 +1,13 @@ +import path from "node:path"; +import { defineProject } from "vitest/config"; + +export default defineProject({ + test: { + exclude: ["node_modules"], + alias: { + "@dicelette/localization": path.resolve(__dirname, "../localization/index.ts"), + "@dicelette/utils": path.resolve(__dirname, "../utils/index.ts"), + "@dicelette/types": path.resolve(__dirname, "../types/index.ts"), + }, + }, +}); diff --git a/packages/types/index.ts b/packages/types/index.ts new file mode 100644 index 00000000..ada5b7b0 --- /dev/null +++ b/packages/types/index.ts @@ -0,0 +1,9 @@ +import type Enmap from "enmap"; +import type { TFunction } from "i18next"; +import type { GuildData } from "./src/database"; + +export type Settings = Enmap; +export type Translation = TFunction<"translation", undefined>; +export * from "./src/database"; +export * from "./src/discord"; +export * from "./src/constants"; diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 00000000..dfee7205 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,20 @@ +{ + "name": "@dicelette/types", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "dist/index.js", + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/Dicelette/discord-dicelette.git" + }, + "private": true, + "dependencies": { + "enmap": "^6.0.3", + "@dicelette/utils": "workspace:*" + } +} diff --git a/src/interfaces/constant.ts b/packages/types/src/constants.ts similarity index 100% rename from src/interfaces/constant.ts rename to packages/types/src/constants.ts diff --git a/src/interfaces/database.ts b/packages/types/src/database.ts similarity index 96% rename from src/interfaces/database.ts rename to packages/types/src/database.ts index b86355eb..a53d642d 100644 --- a/src/interfaces/database.ts +++ b/packages/types/src/database.ts @@ -1,21 +1,6 @@ -import type { Critical } from "@dicelette/core"; - +import type { Critical, CustomCritical } from "@dicelette/core"; import type * as Djs from "discord.js"; -/** - * `[messageId, channelId]` - */ -export type UserMessageId = [string, string]; - -export type PersonnageIds = { channelId: string; messageId: string }; -export type UserRegistration = { - userID: string; - isPrivate?: boolean; - charName?: string | null; - damage?: string[]; - msgId: UserMessageId; -}; - export interface GuildData { /** * Language to use with the bot @@ -99,6 +84,17 @@ export interface GuildData { }; } +export type UserMessageId = [string, string]; + +export type PersonnageIds = { channelId: string; messageId: string }; +export type UserRegistration = { + userID: string; + isPrivate?: boolean; + charName?: string | null; + damage?: string[]; + msgId: UserMessageId; +}; + /** * When a user is registered, a message will be sent in the corresponding channel for the template * When any user roll on a statistique: @@ -121,6 +117,9 @@ export interface UserData { template: { diceType?: string; critical?: Critical; + customCritical?: { + [name: string]: CustomCritical; + }; }; /** * The skill dice that the user can do diff --git a/src/interfaces/discord.ts b/packages/types/src/discord.ts similarity index 60% rename from src/interfaces/discord.ts rename to packages/types/src/discord.ts index bbb3de56..c82d767a 100644 --- a/src/interfaces/discord.ts +++ b/packages/types/src/discord.ts @@ -1,7 +1,4 @@ import type * as Djs from "discord.js"; -import type Enmap from "enmap"; -import type { TFunction } from "i18next"; -import type { GuildData } from "./database"; export type DiscordChannel = | Djs.PrivateThreadChannel @@ -17,6 +14,3 @@ export type DiscordTextChannel = | Djs.PrivateThreadChannel | Djs.PublicThreadChannel | Djs.VoiceChannel; - -export type Settings = Enmap; -export type Translation = TFunction<"translation", undefined>; diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 00000000..aaecd6c7 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./" + }, + "include": ["src", "index.ts"], + "exclude": ["./dist"] +} diff --git a/packages/utils/index.ts b/packages/utils/index.ts new file mode 100644 index 00000000..7af18f9c --- /dev/null +++ b/packages/utils/index.ts @@ -0,0 +1,53 @@ +import { logger } from "./src/logger"; +import "uniformize"; + +export { logger }; + +/** + * filter the choices by removing the accents and check if it includes the removedAccents focused + * @param choices {string[]} + * @param focused {string} + */ +export function filterChoices(choices: string[], focused: string) { + //remove duplicate from choices, without using set + const values = uniqueValues(choices).filter((choice) => + choice.subText(focused.removeAccents()) + ); + if (values.length >= 25) return values.slice(0, 25); + return values; +} + +function uniqueValues(array: string[]) { + const seen: { [key: string]: boolean } = {}; + const uniqueArray: string[] = []; + + for (const item of array) { + const formattedItem = item.standardize(); + if (!seen[formattedItem]) { + seen[formattedItem] = true; + uniqueArray.push(item); + } + } + return uniqueArray; +} + +export function verifyAvatarUrl(url: string) { + if (url.length === 0) return false; + if (url.match(/^(https:)([\/|.\w\s\-_])*(?:jpe?g|gifv?|png|webp)$/gi)) return url; + return false; +} + +/** + * Verify if an array is equal to another + * @param array1 {string[]|undefined} + * @param array2 {string[]|undefined} + */ +export function isArrayEqual(array1: string[] | undefined, array2: string[] | undefined) { + if (!array1 || !array2) return false; + return ( + array1.length === array2.length && + array1.every((value, index) => value === array2[index]) + ); +} + +export * from "./src/errors"; diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 00000000..00ca83e0 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,20 @@ +{ + "name": "@dicelette/utils", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "dist/index.js", + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json" + }, + "repository": { + "type": "git", + "url": "https://github.com/Mara-Li/discord-dicelette.git" + }, + "private": true, + "devDependencies": { + "@types/papaparse": "^5.3.15", + "papaparse": "^5.4.1" + } +} diff --git a/packages/utils/src/errors.ts b/packages/utils/src/errors.ts new file mode 100644 index 00000000..0b43c304 --- /dev/null +++ b/packages/utils/src/errors.ts @@ -0,0 +1,29 @@ +export class NoEmbed extends Error { + constructor() { + super(); + this.name = "NoEmbed"; + } +} + +export class InvalidCsvContent extends Error { + file?: string; + constructor(file?: string) { + super(); + this.name = "InvalidCsvContent"; + this.file = file; + } +} + +export class InvalidURL extends Error { + constructor(url?: string) { + super(url); + this.name = "InvalidURL"; + } +} + +export class NoChannel extends Error { + constructor() { + super(); + this.name = "NoChannel"; + } +} diff --git a/src/logger.ts b/packages/utils/src/logger.ts similarity index 100% rename from src/logger.ts rename to packages/utils/src/logger.ts diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 00000000..680cef4b --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./" + }, + "include": ["./src/**/*", "./src/**/*.json", "index.ts"], + "exclude": ["./dist"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..47a89c4d --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/src/commands/index.ts b/src/commands/index.ts deleted file mode 100644 index 7c9e5564..00000000 --- a/src/commands/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { deleteChar } from "./admin/delete_char"; -import { ADMIN } from "./admin/index"; -import { GIMMICK } from "./gimmick/index"; -import { help } from "./help"; -import { ROLL_AUTO, ROLL_CMDLIST } from "./rolls/index"; -export const autCompleteCmd = [...ROLL_AUTO, ...GIMMICK, deleteChar]; -export const commandsList = [ - ...ROLL_AUTO, - ...ROLL_CMDLIST, - ...GIMMICK, - ...ADMIN, - deleteChar, - help, -]; diff --git a/src/commands/rolls/index.ts b/src/commands/rolls/index.ts deleted file mode 100644 index 8f984bfb..00000000 --- a/src/commands/rolls/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { diceRoll, newScene } from "./base_roll"; -import { dbd } from "./dbAtq"; -import { dbRoll } from "./dbroll"; -import { mjRoll } from "./mj_roll"; - -export const ROLL_AUTO = [dbRoll, dbd, mjRoll]; -export const ROLL_CMDLIST = [diceRoll, newScene]; diff --git a/src/dice.ts b/src/dice.ts deleted file mode 100644 index 7d13903e..00000000 --- a/src/dice.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { Compare, Resultat } from "@dicelette/core"; -import type { Translation } from "@interfaces/discord"; -import { evaluate } from "mathjs"; - -/** - * Parse the result of the dice to be readable - */ -export function parseResult( - output: Resultat, - ul: Translation, - critical?: { failure?: number; success?: number }, - interaction?: boolean -) { - //result is in the form of "d% //comment: [dice] = result" - //parse into - const regexForFormulesDices = /^[✕◈✓]/; - let msgSuccess: string; - const messageResult = output.result.split(";"); - let successOrFailure = ""; - let isCritical: undefined | "failure" | "success" = undefined; - if (output.compare) { - msgSuccess = ""; - let total = 0; - const natural: number[] = []; - for (const r of messageResult) { - if (r.match(regexForFormulesDices)) { - msgSuccess += `${r - .replaceAll(";", "\n") - .replaceAll(":", " ⟶") - .replaceAll(/ = (\S+)/g, " = ` $1 `") - .replaceAll("*", "\\*")}\n`; - continue; - } - const tot = r.match(/ = (\d+)/); - if (tot) { - total = Number.parseInt(tot[1], 10); - } - - successOrFailure = evaluate( - `${total} ${output.compare.sign} ${output.compare.value}` - ) - ? `**${ul("roll.success")}**` - : `**${ul("roll.failure")}**`; - // noinspection RegExpRedundantEscape - const naturalDice = r.matchAll(/\[(\d+)\]/gi); - for (const dice of naturalDice) { - natural.push(Number.parseInt(dice[1], 10)); - } - if (critical) { - if (critical.failure && natural.includes(critical.failure)) { - successOrFailure = `**${ul("roll.critical.failure")}**`; - isCritical = "failure"; - } else if (critical.success && natural.includes(critical.success)) { - successOrFailure = `**${ul("roll.critical.success")}**`; - isCritical = "success"; - } - } - const totalSuccess = output.compare - ? ` = \`${total} ${goodCompareSign(output.compare, total)} [${output.compare.value}]\`` - : `= \`${total}\``; - msgSuccess += `${successOrFailure} — ${r - .replaceAll(":", " ⟶") - .replaceAll(/ = (\S+)/g, totalSuccess) - .replaceAll("*", "\\*")}\n`; - total = 0; - } - } else { - msgSuccess = `${output.result - .replaceAll(";", "\n") - .replaceAll(":", " ⟶") - .replaceAll(/ = (\S+)/g, " = ` $1 `") - .replaceAll("*", "\\*")}`; - } - const comment = output.comment - ? `*${output.comment - .replaceAll(/(\\\*|#|\*\/|\/\*)/g, "") - .replaceAll("×", "*") - .trim()}*\n` - : interaction - ? "\n" - : ""; - const dicesResult = /(?\S+) ⟶ (?.*) =/; - const splitted = msgSuccess.split("\n"); - const finalRes = []; - for (let res of splitted) { - const matches = dicesResult.exec(res); - if (matches) { - const { entry, calc } = matches.groups || {}; - if (entry) { - const entryStr = entry.replaceAll("\\*", "×"); - res = res.replace(entry, `\`${entryStr}\``); - } - if (calc) { - const calcStr = calc.replaceAll("\\*", "×"); - res = res.replace(calc, `\`${calcStr}\``); - } - } - if (isCritical === "failure") { - res = res.replace(regexForFormulesDices, `**${ul("roll.critical.failure")}** —`); - } else if (isCritical === "success") { - res = res.replace(regexForFormulesDices, `**${ul("roll.critical.success")}** —`); - } else { - res = res - .replace("✕", `**${ul("roll.failure")}** —`) - .replace("✓", `**${ul("roll.success")}** —`); - } - - finalRes.push(res.trimStart()); - } - return `${comment} ${finalRes.join("\n ").trimEnd()}`; -} - -/** - * Replace the compare sign as it will invert the result for a better reading - * As the comparaison is after the total (like 20>10) - * @param {Compare} compare - * @param {number} total - */ -function goodCompareSign( - compare: Compare, - total: number -): "<" | ">" | "≥" | "≤" | "=" | "!=" | "==" | "" { - //as the comparaison value is AFTER the total, we need to invert the sign to have a good comparaison string - const { sign, value } = compare; - const success = evaluate(`${total} ${sign} ${value}`); - if (success) { - return sign.replace(">=", "≥").replace("<=", "≤") as - | "<" - | ">" - | "≥" - | "≤" - | "=" - | "" - | "!=" - | "=="; - } - switch (sign) { - case "<": - return ">"; - case ">": - return "<"; - case ">=": - return "≤"; - case "<=": - return "≥"; - case "=": - return "="; - case "!=": - return "!="; - case "==": - return "=="; - default: - return ""; - } -} diff --git a/src/events/message_create.ts b/src/events/message_create.ts deleted file mode 100644 index fffa81e3..00000000 --- a/src/events/message_create.ts +++ /dev/null @@ -1,123 +0,0 @@ -// noinspection RegExpRedundantEscape - -import { COMMENT_REGEX, type Resultat, roll } from "@dicelette/core"; -import { lError, ln } from "@localization"; -import { logger } from "@logger"; -import type { EClient } from "@main"; -import { deleteAfter, timestamp } from "@utils"; -import { findForumChannel, findMessageBefore, findThread } from "@utils/find"; -import * as Djs from "discord.js"; -import { parseResult } from "../dice"; - -export const DETECT_DICE_MESSAGE = /([\w\.]+|(\{.*\})) (.*)/i; - -export default (client: EClient): void => { - client.on("messageCreate", async (message) => { - try { - if (message.author.bot) return; - if (message.channel.type === Djs.ChannelType.DM) return; - if (!message.guild) return; - let content = message.content; - //detect roll between bracket - const detectRoll = content.match(/\[(.*)\]/)?.[1]; - const comments = content.match(DETECT_DICE_MESSAGE)?.[3].replaceAll("*", "\\*"); - if (comments && !detectRoll) { - const diceValue = content.match(/^\S*#?d\S+|\{.*\}/i); - if (!diceValue) return; - content = content.replace(DETECT_DICE_MESSAGE, "$1"); - } - let deleteInput = true; - let result: Resultat | undefined; - try { - result = detectRoll - ? roll(detectRoll.toLowerCase()) - : roll(content.toLowerCase()); - } catch (e) { - return; - } - if (detectRoll) { - deleteInput = false; - } - - //is a valid roll as we are in the function so we can work as always - const userLang = - client.settings.get(message.guild.id, "lang") ?? - message.guild.preferredLocale ?? - Djs.Locale.EnglishUS; - const ul = ln(userLang); - const channel = message.channel; - if (!result) return; - if (comments && !detectRoll && result) { - result.dice = `${result.dice} /* ${comments} */`; - result.comment = comments; - } - const parser = parseResult(result, ul); - if ( - channel.name.startsWith("🎲") || - client.settings.get(message.guild.id, "disableThread") === true || - client.settings.get(message.guild.id, "rollChannel") === channel.id - ) { - await message.reply({ content: parser, allowedMentions: { repliedUser: true } }); - return; - } - let linkToOriginal = ""; - if (deleteInput) { - if (client.settings.get(message.guild.id, "context")) { - const messageBefore = await findMessageBefore(channel, message, client); - if (messageBefore) - linkToOriginal = `\n-# ↪ [${ul("common.context")}]()`; - } - } else { - linkToOriginal = `\n-# ↪ [${ul("common.context")}] (<${message.url}>)`; - } - const parentChannel = - channel instanceof Djs.ThreadChannel ? channel.parent : channel; - const thread = - parentChannel instanceof Djs.TextChannel - ? await findThread(client.settings, parentChannel, ul) - : await findForumChannel( - parentChannel as Djs.ForumChannel, - channel as Djs.ThreadChannel, - client.settings, - ul - ); - const msgToEdit = await thread.send("_ _"); - const signMessage = result.compare - ? `${result.compare.sign} ${result.compare.value}` - : ""; - const authorMention = `*${Djs.userMention(message.author.id)}* (🎲 \`${result.dice.replace(COMMENT_REGEX, "")}${signMessage ? ` ${signMessage}` : ""}\`)`; - const msg = `${authorMention}${timestamp(client.settings, message.guild.id)}\n${parser}${linkToOriginal}`; - await msgToEdit.edit(msg); - const idMessage = client.settings.get(message.guild.id, "linkToLogs") - ? `\n\n-# ↪ ${msgToEdit.url}` - : ""; - const reply = deleteInput - ? await channel.send({ content: `${authorMention}\n${parser}${idMessage}` }) - : await message.reply({ - content: `${parser}${idMessage}`, - allowedMentions: { repliedUser: true }, - }); - const timer = client.settings.get(message.guild.id, "deleteAfter") ?? 180000; - await deleteAfter(reply, timer); - if (deleteInput) await message.delete(); - return; - } catch (e) { - logger.error(e); - if (!message.guild) return; - const userLang = - client.settings.get(message.guild.id, "lang") ?? - message.guild.preferredLocale ?? - Djs.Locale.EnglishUS; - const msgError = lError(e as Error, undefined, userLang); - if (msgError.length === 0) return; - await message.channel.send({ content: msgError }); - const logsId = client.settings.get(message.guild.id, "logs"); - if (logsId) { - const logs = await message.guild.channels.fetch(logsId); - if (logs instanceof Djs.TextChannel) { - await logs.send(`\`\`\`\n${(e as Error).message}\n\`\`\``); - } - } - } - }); -}; diff --git a/src/events/on_delete.ts b/src/events/on_delete.ts deleted file mode 100644 index 5d9601a7..00000000 --- a/src/events/on_delete.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { GuildData, PersonnageIds } from "@interfaces/database"; -import { logger } from "@logger"; -import type { EClient } from "@main"; - -import { sendLogs } from "@utils"; -import type { AnyThreadChannel } from "discord.js"; -import type * as Djs from "discord.js"; -import type Enmap from "enmap"; -export const DELETE_CHANNEL = (client: EClient): void => { - client.on("channelDelete", async (channel) => { - try { - if (channel.isDMBased()) return; - const guildID = channel.guild.id; - const db = client.settings; - deleteIfChannelOrThread(db, guildID, channel); - } catch (error) { - logger.error(error); - if (channel.isDMBased()) return; - await sendLogs((error as Error).message, channel.guild, client.settings); - } - }); -}; - -function deleteIfChannelOrThread( - db: Enmap, - guildID: string, - channel: Djs.NonThreadGuildBasedChannel | AnyThreadChannel -) { - const channelID = channel.id; - cleanUserDB(db, channel); - if (db.get(guildID, "templateID.channelId") === channelID) - db.delete(guildID, "templateID"); - if (db.get(guildID, "logs") === channelID) db.delete(guildID, "logs"); - if (db.get(guildID, "managerId") === channelID) db.delete(guildID, "managerId"); - if (db.get(guildID, "privateChannel") === channelID) - db.delete(guildID, "privateChannel"); - if (db.get(guildID, "rollChannel") === channelID) db.delete(guildID, "rollChannel"); -} - -export const DELETE_THREAD = (client: EClient): void => { - client.on("threadDelete", async (thread) => { - try { - //search channelID in database and delete it - const guildID = thread.guild.id; - const db = client.settings; - //verify if the user message was in the thread - deleteIfChannelOrThread(db, guildID, thread); - } catch (error) { - logger.error(error); - if (thread.isDMBased()) return; - await sendLogs((error as Error).message, thread.guild, client.settings); - } - }); -}; - -export const DELETE_MESSAGE = (client: EClient): void => { - client.on("messageDelete", async (message) => { - try { - if (!message.guild) return; - const messageId = message.id; - //search channelID in database and delete it - const guildID = message.guild.id; - const channel = message.channel; - if (channel.isDMBased()) return; - if (client.settings.get(guildID, "templateID.messageId") === messageId) - client.settings.delete(guildID, "templateID"); - - const dbUser = client.settings.get(guildID, "user"); - if (dbUser && Object.keys(dbUser).length > 0) { - for (const [user, values] of Object.entries(dbUser)) { - for (const [index, value] of values.entries()) { - const persoId: PersonnageIds = { - messageId: value.messageId[0], - channelId: value.messageId[1], - }; - if (persoId.messageId === messageId && persoId.channelId === channel.id) { - logger.silly(`Deleted character ${value.charName} for user ${user}`); - values.splice(index, 1); - } - } - if (values.length === 0) delete dbUser[user]; - } - } - client.settings.set(guildID, dbUser, "user"); - } catch (error) { - if (!message.guild) return; - sendLogs((error as Error).message, message.guild, client.settings); - } - }); -}; - -export const ON_KICK = (client: EClient): void => { - client.on("guildDelete", async (guild) => { - //delete guild from database - try { - client.settings.delete(guild.id); - } catch (error) { - logger.error(error); - } - }); -}; - -function cleanUserDB( - guildDB: Enmap, - thread: Djs.GuildTextBasedChannel | Djs.ThreadChannel | Djs.NonThreadGuildBasedChannel -) { - const dbUser = guildDB.get(thread.guild.id, "user"); - if (!dbUser) return; - if (!thread.isTextBased()) return; - /** if private channel was deleted, delete only the private charactersheet */ - - for (const [user, data] of Object.entries(dbUser)) { - const filterChar = data.filter((char) => { - return char.messageId[1] !== thread.id; - }); - logger.silly( - `Deleted ${data.length - filterChar.length} characters for user ${user}` - ); - if (filterChar.length === 0) guildDB.delete(thread.guild.id, `user.${user}`); - else guildDB.set(thread.guild.id, filterChar, `user.${user}`); - } -} - -export function deleteUser( - interaction: Djs.CommandInteraction | Djs.ModalSubmitInteraction, - guildData: GuildData, - user?: Djs.User | null, - charName?: string | null -) { - //delete the character from the database - const userCharIndex = guildData.user[user?.id ?? interaction.user.id].findIndex( - (char) => { - return char.charName?.standardize() === charName?.standardize(); - } - ); - if (userCharIndex === -1) { - return guildData; - } - guildData.user[user?.id ?? interaction.user.id].splice(userCharIndex, 1); - return guildData; -} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 6c88fed9..00000000 --- a/src/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -import * as process from "node:process"; -import * as Djs from "discord.js"; -import dotenv from "dotenv"; -import Enmap from "enmap"; -import "uniformize"; -import { logger } from "@logger"; -//group type & interface -import type { GuildData } from "@interfaces/database"; - -//group: Import @events -import interaction from "@events/interaction"; -import join from "@events/join"; -import MESSAGE_CREATE from "@events/message_create"; -import { - DELETE_CHANNEL, - DELETE_MESSAGE, - DELETE_THREAD, - ON_KICK, -} from "@events/on_delete"; -import ready from "@events/ready"; -import { onReactionAdd, onReactionRemove } from "@events/MessageReactionAdd"; - -// Load the environment variables -dotenv.config({ path: ".env" }); -logger.info("Starting bot..."); - -export class EClient extends Djs.Client { - // Déclaration d'une propriété settings avec le type Enmap - public settings: Enmap; - - constructor(options: Djs.ClientOptions) { - super(options); - - // Initialisation de Enmap et attachement au client - this.settings = new Enmap({ - name: "settings", - fetchAll: false, - autoFetch: true, - cloneLevel: "deep", - }); - } -} - -export const client = new EClient({ - intents: [ - Djs.GatewayIntentBits.GuildMessages, - Djs.GatewayIntentBits.MessageContent, - Djs.GatewayIntentBits.Guilds, - Djs.GatewayIntentBits.GuildMembers, - Djs.GatewayIntentBits.GuildMessageReactions, - ], - partials: [ - Djs.Partials.Channel, - Djs.Partials.GuildMember, - Djs.Partials.User, - Djs.Partials.Reaction, - Djs.Partials.User, - ], -}); -//@ts-ignore -export const VERSION = process.env.npm_package_version ?? "/"; - -try { - ready(client); - interaction(client); - join(client); - MESSAGE_CREATE(client); - ON_KICK(client); - DELETE_MESSAGE(client); - DELETE_CHANNEL(client); - DELETE_THREAD(client); - onReactionAdd(client); - onReactionRemove(client); -} catch (error) { - console.error(error); -} - -// noinspection JSIgnoredPromiseFromCall -client.login(process.env.DISCORD_TOKEN); diff --git a/src/interactions/add/stats.ts b/src/interactions/add/stats.ts deleted file mode 100644 index dc54ad6a..00000000 --- a/src/interactions/add/stats.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { type StatisticalTemplate, evalCombinaison } from "@dicelette/core"; -import { ln } from "@localization"; -import { reply } from "@utils"; -import { continueCancelButtons, registerDmgButton } from "@utils/buttons"; -import { getEmbeds, getStatistiqueFields } from "@utils/parse"; -import * as Djs from "discord.js"; -import { createStatsEmbed } from ".."; -/** - * Embed to display the statistics when adding a new user - * @param interaction {Djs.ModalSubmitInteraction} - * @param template {StatisticalTemplate} - * @param page {number=2} - * @param lang - */ -export async function embedStatistiques( - interaction: Djs.ModalSubmitInteraction, - template: StatisticalTemplate, - page: number | undefined = 2, - lang: Djs.Locale = Djs.Locale.EnglishGB -) { - if (!interaction.message) return; - const ul = ln(lang); - const userEmbed = getEmbeds(ul, interaction.message, "user"); - if (!userEmbed) return; - const statsEmbed = getEmbeds(ul, interaction.message, "stats"); - const { combinaisonFields, stats } = getStatistiqueFields(interaction, template, ul); - //combine all embeds as one - userEmbed.setFooter({ text: ul("common.page", { nb: page }) }); - //add old fields - - const statEmbeds = statsEmbed ?? createStatsEmbed(ul); - for (const [stat, value] of Object.entries(stats)) { - statEmbeds.addFields({ - name: stat.capitalize(), - value: `\`${value}\``, - inline: true, - }); - } - const statsWithoutCombinaison = template.statistics - ? Object.keys(template.statistics) - .filter((stat) => !template.statistics![stat].combinaison) - .map((name) => name.standardize()) - : []; - const embedObject = statEmbeds.toJSON(); - const fields = embedObject.fields; - if (!fields) return; - const parsedFields: { [name: string]: string } = {}; - for (const field of fields) { - parsedFields[field.name.standardize()] = field.value.removeBacktick().standardize(); - } - - const embedStats = Object.fromEntries( - Object.entries(parsedFields).filter(([key]) => statsWithoutCombinaison.includes(key)) - ); - if (Object.keys(embedStats).length === statsWithoutCombinaison.length) { - // noinspection JSUnusedAssignment - let combinaison: { [name: string]: number } = {}; - combinaison = evalCombinaison(combinaisonFields, embedStats); - //add combinaison to the embed - for (const stat of Object.keys(combinaison)) { - statEmbeds.addFields({ - name: stat.capitalize(), - value: `\`${combinaisonFields[stat]}\` = ${combinaison[stat]}`, - inline: true, - }); - } - - await interaction.message.edit({ - embeds: [userEmbed, statEmbeds], - components: [registerDmgButton(ul)], - }); - await reply(interaction, { content: ul("modals.added.stats"), ephemeral: true }); - return; - } - await interaction.message.edit({ - embeds: [userEmbed, statEmbeds], - components: [continueCancelButtons(ul)], - }); - await reply(interaction, { content: ul("modals.added.stats"), ephemeral: true }); - return; -} - -/** - * Modal to display the statistics when adding a new user - * Will display the statistics that are not already set - * 5 statistics per page - */ -export async function showStatistiqueModal( - interaction: Djs.ButtonInteraction, - template: StatisticalTemplate, - stats?: string[], - page = 1 -) { - if (!template.statistics) return; - const ul = ln(interaction.locale as Djs.Locale); - const statsWithoutCombinaison = - Object.keys(template.statistics).filter((stat) => { - return !template.statistics?.[stat]?.combinaison; - }) ?? []; - const nbOfPages = - Math.ceil(statsWithoutCombinaison.length / 5) >= 1 - ? Math.ceil(statsWithoutCombinaison.length / 5) - : page; - const modal = new Djs.ModalBuilder() - .setCustomId(`page${page}`) - .setTitle(ul("modals.steps", { page, max: nbOfPages + 1 })); - let statToDisplay = statsWithoutCombinaison; - if (stats && stats.length > 0) { - statToDisplay = statToDisplay.filter((stat) => !stats.includes(stat.unidecode())); - if (statToDisplay.length === 0) { - //remove button - const button = registerDmgButton(ul); - await reply(interaction, { content: ul("modals.alreadySet"), ephemeral: true }); - await interaction.message.edit({ components: [button] }); - } - } - const statsToDisplay = statToDisplay.slice(0, 4); - const statisticsLowerCase = Object.fromEntries( - Object.entries(template.statistics).map(([key, value]) => [key.standardize(), value]) - ); - for (const stat of statsToDisplay) { - const cleanedName = stat.unidecode(); - const value = statisticsLowerCase[cleanedName]; - if (value.combinaison) continue; - let msg = ""; - if (value.min && value.max) - msg = ul("modals.enterValue.minAndMax", { min: value.min, max: value.max }); - else if (value.min) msg = ul("modals.enterValue.minOnly", { min: value.min }); - else if (value.max) msg = ul("modals.enterValue.maxOnly", { max: value.max }); - const input = - new Djs.ActionRowBuilder().addComponents( - new Djs.TextInputBuilder() - .setCustomId(cleanedName) - .setLabel(stat) - .setPlaceholder(msg) - .setRequired(true) - .setValue("") - .setStyle(Djs.TextInputStyle.Short) - ); - modal.addComponents(input); - } - await interaction.showModal(modal); -} diff --git a/src/interactions/edit/avatar.ts b/src/interactions/edit/avatar.ts deleted file mode 100644 index 867fc19b..00000000 --- a/src/interactions/edit/avatar.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { allowEdit } from "@interactions"; -import type { Settings, Translation } from "@interfaces/discord"; -import { findln } from "@localization"; -import { embedError, reply } from "@utils"; -import { getEmbeds, getEmbedsList } from "@utils/parse"; -import * as Djs from "discord.js"; -import { verifyAvatarUrl } from "@register/validate"; -export async function initiateAvatarEdit( - interaction: Djs.StringSelectMenuInteraction, - ul: Translation, - interactionUser: Djs.User, - db: Settings -) { - if (await allowEdit(interaction, db, interactionUser)) - await showAvatarEdit(interaction, ul); -} - -export async function showAvatarEdit( - interaction: Djs.StringSelectMenuInteraction, - ul: Translation -) { - const embed = getEmbeds(ul, interaction.message, "user"); - if (!embed) throw new Error(ul("error.noEmbed")); - const thumbnail = embed.toJSON().thumbnail?.url ?? interaction.user.displayAvatarURL(); - const modal = new Djs.ModalBuilder() - .setCustomId("editAvatar") - .setTitle(ul("button.avatar.description")); - const input = - new Djs.ActionRowBuilder().addComponents( - new Djs.TextInputBuilder() - .setCustomId("avatar") - .setLabel(ul("modals.avatar.name")) - .setRequired(true) - .setStyle(Djs.TextInputStyle.Short) - .setValue(thumbnail) - ); - modal.addComponents(input); - await interaction.showModal(modal); -} - -export async function validateAvatarEdit( - interaction: Djs.ModalSubmitInteraction, - ul: Translation -) { - if (!interaction.message) return; - const avatar = interaction.fields.getTextInputValue("avatar"); - if (avatar.match(/(cdn|media)\.discordapp\.net/gi)) - return await reply(interaction, { - embeds: [embedError(ul("error.avatar.discord"), ul)], - }); - if (!verifyAvatarUrl(avatar)) - return await reply(interaction, { embeds: [embedError(ul("error.avatar.url"), ul)] }); - - const embed = getEmbeds(ul, interaction.message, "user"); - if (!embed) throw new Error(ul("error.noEmbed")); - embed.setThumbnail(avatar); - const embedsList = getEmbedsList(ul, { which: "user", embed }, interaction.message); - await interaction.message.edit({ embeds: embedsList.list }); - const user = embed - .toJSON() - .fields?.find((field) => findln(field.name) === "common.user")?.value; - const charName = embed - .toJSON() - .fields?.find((field) => findln(field.name) === "common.character")?.value; - const nameMention = - !charName || findln(charName) === "common.noSet" ? user : `${user} (${charName})`; - const msgLink = interaction.message.url; - await reply(interaction, { - content: ul("edit_avatar.success", { name: nameMention, link: msgLink }), - ephemeral: true, - }); -} diff --git a/src/interactions/index.ts b/src/interactions/index.ts deleted file mode 100644 index 1a4898c1..00000000 --- a/src/interactions/index.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { PersonnageIds } from "@interfaces/database"; -import type { Settings, Translation } from "@interfaces/discord"; -import { findln, ln } from "@localization"; -import { logger } from "@logger"; -import { embedError, reply } from "@utils"; -import { ensureEmbed, getEmbeds } from "@utils/parse"; -import * as Djs from "discord.js"; -/** - * Get the userName and the char from the embed between an interaction (button or modal), throw error if not found - */ -export async function getUserNameAndChar( - interaction: Djs.ButtonInteraction | Djs.ModalSubmitInteraction, - ul: Translation, - first?: boolean -) { - let userEmbed = getEmbeds(ul, interaction?.message ?? undefined, "user"); - if (first) { - const firstEmbed = ensureEmbed(interaction?.message ?? undefined); - if (firstEmbed) userEmbed = new Djs.EmbedBuilder(firstEmbed.toJSON()); - } - if (!userEmbed) throw new Error(ul("error.noEmbed")); - const userID = userEmbed - .toJSON() - .fields?.find((field) => findln(field.name) === "common.user") - ?.value.replace("<@", "") - .replace(">", ""); - if (!userID) throw new Error(ul("error.user")); - if ( - !interaction.channel || - (!(interaction.channel instanceof Djs.ThreadChannel) && - !(interaction.channel instanceof Djs.TextChannel)) - ) - throw new Error(ul("error.noThread")); - let userName = userEmbed - .toJSON() - .fields?.find((field) => findln(field.name) === "common.character")?.value; - if (userName === ul("common.noSet")) userName = undefined; - return { userID, userName, thread: interaction.channel }; -} -/** - * Create the dice skill embed - * @param ul {Translation} - */ -export function createDiceEmbed(ul: Translation) { - return new Djs.EmbedBuilder().setTitle(ul("embed.dice")).setColor("Green"); -} - -/** - * Create the userEmbed and embedding the avatar user in the thumbnail - * @param ul {Translation} - * @param thumbnail {string} The avatar of the user in the server (use server profile first, after global avatar) - * @param user - * @param charName - */ -export function createUserEmbed( - ul: Translation, - thumbnail: string | null, - user: string, - charName?: string -) { - const userEmbed = new Djs.EmbedBuilder() - .setTitle(ul("embed.user")) - .setColor("Random") - .setThumbnail(thumbnail) - .addFields({ - name: ul("common.user").capitalize(), - value: `<@${user}>`, - inline: true, - }); - if (charName) - userEmbed.addFields({ - name: ul("common.character").capitalize(), - value: charName.capitalize(), - inline: true, - }); - else - userEmbed.addFields({ - name: ul("common.character").capitalize(), - value: ul("common.noSet").capitalize(), - inline: true, - }); - return userEmbed; -} - -/** - * Create the statistic embed - * @param ul {Translation} - */ -export function createStatsEmbed(ul: Translation) { - return new Djs.EmbedBuilder() - .setTitle(ul("common.statistics").capitalize()) - .setColor("Aqua"); -} - -/** - * Create the template embed for user - * @param ul {Translation} - */ -export function createTemplateEmbed(ul: Translation) { - return new Djs.EmbedBuilder().setTitle(ul("embed.template")).setColor("DarkGrey"); -} - -export function verifyIfEmbedInDB( - db: Settings, - message: Djs.Message, - userId: string, - userName?: string -): { isInDb: boolean; coord?: PersonnageIds } { - const charData = db.get(message.guild!.id, `user.${userId}`); - if (!charData) return { isInDb: false }; - const charName = charData.find((char) => { - if (userName && char.charName) - return char.charName.standardize() === userName.standardize(); - return char.charName == null && userName == null; - }); - if (!charName) return { isInDb: false }; - const ids: PersonnageIds = { - channelId: charName.messageId[1], - messageId: charName.messageId[0], - }; - return { - isInDb: message.channel.id === ids.channelId && message.id === ids.messageId, - coord: ids, - }; -} - -export async function allowEdit( - interaction: Djs.ButtonInteraction | Djs.StringSelectMenuInteraction, - db: Settings, - interactionUser: Djs.User -) { - const ul = ln(interaction.locale as Djs.Locale); - const embed = ensureEmbed(interaction.message); - const user = embed.fields - .find((field) => findln(field.name) === "common.user") - ?.value.replace("<@", "") - .replace(">", ""); - const isSameUser = user === interactionUser.id; - const isModerator = interaction.guild?.members.cache - .get(interactionUser.id) - ?.permissions.has(Djs.PermissionsBitField.Flags.ManageRoles); - const first = interaction.customId.includes("first"); - const userName = embed.fields.find((field) => - ["common.character", "common.charName"].includes(findln(field.name)) - ); - const userNameValue = - userName && findln(userName?.value) === "common.noSet" ? undefined : userName?.value; - if (!first && user) { - const { isInDb, coord } = verifyIfEmbedInDB( - db, - interaction.message, - user, - userNameValue - ); - if (!isInDb) { - const urlNew = `https://discord.com/channels/${interaction.guild!.id}/${coord?.channelId}/${coord?.messageId}`; - await reply(interaction, { - embeds: [embedError(ul("error.oldEmbed", { fiche: urlNew }), ul)], - ephemeral: true, - }); - //delete the message - try { - await interaction.message.delete(); - } catch (e) { - logger.warn("Error while deleting message", e, "allowEdit"); - } - return false; - } - } - if (isSameUser || isModerator) return true; - await reply(interaction, { content: ul("modals.noPermission"), ephemeral: true }); - return false; -} diff --git a/src/localizations/init.ts b/src/localizations/init.ts deleted file mode 100644 index b743637d..00000000 --- a/src/localizations/init.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as Djs from "discord.js"; -import i18next from "i18next"; -import EnglishUS from "./locales/en.json" assert { type: "json" }; -import French from "./locales/fr.json" assert { type: "json" }; - -export const resources = { - en: { - translation: EnglishUS, - }, - fr: { - translation: French, - }, -}; - -// noinspection JSIgnoredPromiseFromCall -i18next.init({ - lng: "en", - fallbackLng: "en", - returnNull: false, - resources, -}); - -export enum LocalePrimary { - // noinspection JSUnusedGlobalSymbols - French = "Français", - English = "English", -} - -export const localeList = Object.keys(Djs.Locale) - .map((key) => { - return { - name: key, - value: Djs.Locale[key as keyof typeof Djs.Locale], - }; - }) - .filter((x) => Object.keys(resources).includes(x.value)) - .map((x) => { - return { - name: LocalePrimary[x.name as keyof typeof LocalePrimary], - value: x.value as Djs.Locale, - }; - }); -localeList.push({ name: LocalePrimary.English, value: Djs.Locale.EnglishUS }); diff --git a/src/utils/db.ts b/src/utils/db.ts deleted file mode 100644 index 94774aba..00000000 --- a/src/utils/db.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { type StatisticalTemplate, verifyTemplateValue } from "@dicelette/core"; -import { templateSchema } from "@dicelette/core"; -import type { PersonnageIds, UserData, UserRegistration } from "@interfaces/database"; -import type { Settings, Translation } from "@interfaces/discord"; -import { ln } from "@localization"; -import type { EClient } from "@main"; -import { embedError, haveAccess, reply, searchUserChannel } from "@utils"; -import { ensureEmbed, getEmbeds, parseEmbedFields } from "@utils/parse"; -import * as Djs from "discord.js"; - -/** - * Get the guild template when clicking on the "registering user" button or when submitting - */ -export async function getTemplate( - interaction: Djs.ButtonInteraction | Djs.ModalSubmitInteraction -): Promise { - const template = interaction.message?.attachments.first(); - if (!template) return; - const res = await fetch(template.url).then((res) => res.json()); - return verifyTemplateValue(res); -} - -export function serializeName( - userStatistique: UserData | undefined, - charName: string | undefined -) { - const serializedNameDB = userStatistique?.userName?.standardize(true); - const serializedNameQueries = charName?.standardize(true); - return ( - serializedNameDB !== serializedNameQueries || - (serializedNameQueries && serializedNameDB?.includes(serializedNameQueries)) - ); -} - -export async function getDatabaseChar( - interaction: Djs.CommandInteraction, - client: EClient, - t: Translation, - strict = true -) { - const options = interaction.options as Djs.CommandInteractionOptionResolver; - const guildData = client.settings.get(interaction.guildId as string); - const ul = ln(interaction.locale as Djs.Locale); - if (!guildData) { - await reply(interaction, { embeds: [embedError(ul("error.noTemplate"), ul)] }); - return undefined; - } - const user = options.getUser(t("display.userLowercase")); - let charName = options.getString(t("common.character"))?.toLowerCase(); - if (charName?.includes(ul("common.default").toLowerCase())) charName = undefined; - - if (!user && charName) { - //get the character data in the database - const allUsersData = guildData.user; - const allUsers = Object.entries(allUsersData); - for (const [user, data] of allUsers) { - const userChar = data.find((char) => { - return char.charName?.subText(charName, strict); - }); - if (userChar) { - return { - [user as string]: userChar, - }; - } - } - } - const userData = client.settings.get( - interaction.guild!.id, - `user.${user?.id ?? interaction.user.id}` - ); - const findChara = userData?.find((char) => { - if (charName) return char.charName?.subText(charName, strict); - }); - if (!findChara && charName) { - return undefined; - } - if (!findChara) { - const char = userData?.[0]; - - return char ? { [user?.id ?? interaction.user.id]: char } : undefined; - } - return { - [user?.id ?? interaction.user.id]: findChara, - }; -} - -/** - * Get the statistical Template using the database templateID information - */ -export async function getTemplateWithDB( - interaction: - | Djs.ButtonInteraction - | Djs.ModalSubmitInteraction - | Djs.CommandInteraction, - enmap: Settings -) { - if (!interaction.guild) return; - const guild = interaction.guild; - const templateID = enmap.get(interaction.guild.id, "templateID"); - const ul = ln(interaction.locale); - if (!enmap.has(interaction.guild.id) || !templateID) - throw new Error(ul("error.noGuildData", { server: interaction.guild.name })); - - const { channelId, messageId } = templateID; - const channel = await guild.channels.fetch(channelId); - if (!channel || channel instanceof Djs.CategoryChannel) return; - try { - const message = await channel.messages.fetch(messageId); - const template = message.attachments.first(); - if (!template) { - // noinspection ExceptionCaughtLocallyJS - throw new Error(ul("error.noTemplate")); - } - const res = await fetch(template.url).then((res) => res.json()); - return parseTemplate(enmap, guild.id, res); - } catch (error) { - if ((error as Error).message === "Unknown Message") - throw new Error(ul("error.noTemplateId", { channelId, messageId })); - throw error; - } -} - -//Get only the template value without testing it -// biome-ignore lint/suspicious/noExplicitAny: -function parseTemplate(db: Settings, guildId: string, template: any) { - if (!db.get(guildId, "templateID.valid")) { - db.set(guildId, true, "templateID.valid"); - return verifyTemplateValue(template); - } - const parsedTemplate = templateSchema.parse(template); - return parsedTemplate as StatisticalTemplate; -} - -/** - * Create the UserData starting from the guildData and using a userId - */ -export async function getUserFromMessage( - guildData: Settings, - userId: string, - interaction: Djs.BaseInteraction, - charName?: string | null, - options?: { - integrateCombinaison?: boolean; - allowAccess?: boolean; - skipNotFound?: boolean; - fetchAvatar?: boolean; - fetchChannel?: boolean; - } -) { - if (!options) - //biome-ignore lint/style/noParameterAssign: We need to assign a default value - options = { integrateCombinaison: true, allowAccess: true, skipNotFound: false }; - const { integrateCombinaison, allowAccess, skipNotFound } = options; - const ul = ln(interaction.locale); - const guild = interaction.guild; - const user = guildData.get(guild!.id, `user.${userId}`)?.find((char) => { - return char.charName?.subText(charName); - }); - if (!user) return; - const userMessageId: PersonnageIds = { - channelId: user.messageId[1], - messageId: user.messageId[0], - }; - const thread = await searchUserChannel( - guildData, - interaction, - ul, - userMessageId.channelId - ); - if (!thread) throw new Error(ul("error.noThread")); - if (user.isPrivate && !allowAccess && !haveAccess(interaction, thread.id, userId)) { - throw new Error(ul("error.private")); - } - try { - const message = await thread.messages.fetch(userMessageId.messageId); - return getUserByEmbed( - message, - ul, - undefined, - integrateCombinaison, - options.fetchAvatar, - options.fetchChannel - ); - } catch (error) { - if (!skipNotFound) throw new Error(ul("error.user"), { cause: "404 not found" }); - } -} - -/** - * Register an user in the database - * @returns - */ -export async function registerUser( - userData: UserRegistration, - interaction: Djs.BaseInteraction, - enmap: Settings, - deleteMsg: boolean | undefined = true, - errorOnDuplicate: boolean | undefined = false -) { - const { userID, charName, msgId, isPrivate, damage } = userData; - const ids: PersonnageIds = { channelId: msgId[1], messageId: msgId[0] }; - if (!interaction.guild) return; - const guildData = enmap.get(interaction.guild.id); - if (!guildData) return; - if (!guildData.user) guildData.user = {}; - - const user = enmap.get(interaction.guild.id, `user.${userID}`); - const newChar = { - charName, - messageId: msgId, - damageName: damage, - isPrivate, - }; - //biome-ignore lint/performance/noDelete: We need to delete the key if it's not needed (because we are registering in the DB and undefined can lead to a bug) - if (!charName) delete newChar.charName; - //biome-ignore lint/performance/noDelete: We need to delete the key if it's not needed (because we are registering in the DB and undefined can lead to a bug) - if (!damage) delete newChar.damageName; - if (user) { - const char = user.find((char) => { - return char.charName?.subText(charName, true); - }); - const charIndex = user.findIndex((char) => { - return char.charName?.subText(charName, true); - }); - if (char) { - if (errorOnDuplicate) throw new Error("DUPLICATE"); - //delete old message - if (deleteMsg) { - try { - const threadOfChar = await searchUserChannel( - enmap, - interaction, - ln(interaction.locale), - ids.channelId - ); - if (threadOfChar) { - const oldMessage = await threadOfChar.messages.fetch(char.messageId[1]); - if (oldMessage) oldMessage.delete(); - } - } catch (error) { - //skip unknown message - } - } - //overwrite the message id - char.messageId = msgId; - if (damage) char.damageName = damage; - enmap.set(interaction.guild.id, char, `user.${userID}.${charIndex}`); - } else enmap.set(interaction.guild.id, [...user, newChar], `user.${userID}`); - return; - } - enmap.set(interaction.guild.id, [newChar], `user.${userID}`); -} - -/** - * Get the userData from the embed - */ -export function getUserByEmbed( - message: Djs.Message, - ul: Translation, - first: boolean | undefined = false, - integrateCombinaison = true, - fetchAvatar = false, - fetchChannel = false -) { - const user: Partial = {}; - const userEmbed = first ? ensureEmbed(message) : getEmbeds(ul, message, "user"); - if (!userEmbed) return; - const parsedFields = parseEmbedFields(userEmbed.toJSON() as Djs.Embed); - const charNameFields = [ - { key: "common.charName", value: parsedFields?.["common.charName"] }, - { key: "common.character", value: parsedFields?.["common.character"] }, - ].find((field) => field.value !== undefined); - if (charNameFields && charNameFields.value !== "common.noSet") { - user.userName = charNameFields.value; - } - const statsFields = getEmbeds(ul, message, "stats")?.toJSON()?.fields; - let stats: { [name: string]: number } | undefined = undefined; - if (statsFields) { - stats = {}; - for (const stat of statsFields) { - const value = Number.parseInt(stat.value.removeBacktick(), 10); - if (Number.isNaN(value)) { - //it's a combinaison - //remove the `x` = text; - const combinaison = stat.value.split("=")[1].trim(); - if (integrateCombinaison) - stats[stat.name.unidecode()] = Number.parseInt(combinaison, 10); - } else stats[stat.name.unidecode()] = value; - } - } - user.stats = stats; - const damageFields = getEmbeds(ul, message, "damage")?.toJSON()?.fields; - let templateDamage: { [name: string]: string } | undefined = undefined; - if (damageFields) { - templateDamage = {}; - for (const damage of damageFields) { - templateDamage[damage.name.unidecode()] = damage.value.removeBacktick(); - } - } - const templateEmbed = first ? userEmbed : getEmbeds(ul, message, "template"); - const templateFields = parseEmbedFields(templateEmbed?.toJSON() as Djs.Embed); - user.damage = templateDamage; - user.template = { - diceType: templateFields?.["common.dice"] || undefined, - critical: { - success: Number.parseInt(templateFields?.["roll.critical.success"], 10), - failure: Number.parseInt(templateFields["roll.critical.failure"], 10), - }, - }; - if (fetchAvatar) user.avatar = userEmbed.toJSON().thumbnail?.url || undefined; - if (fetchChannel) user.channel = message.channel.id; - return user as UserData; -} - -/** - * Register the managerId in the database - */ -export function setDefaultManagerId( - guildData: Settings, - interaction: Djs.BaseInteraction, - channel?: string -) { - if (!channel || !interaction.guild) return; - guildData.set(interaction.guild.id, channel, "managerId"); -} - -export async function getFirstRegisteredChar( - client: EClient, - interaction: Djs.CommandInteraction, - ul: Translation -) { - const userData = client.settings.get( - interaction.guild!.id, - `user.${interaction.user.id}` - ); - if (!userData) { - await reply(interaction, { - embeds: [embedError(ul("error.notRegistered"), ul)], - ephemeral: true, - }); - return; - } - const firstChar = userData[0]; - const optionChar = firstChar.charName?.capitalize(); - const userStatistique = await getUserFromMessage( - client.settings, - interaction.user.id, - interaction, - firstChar.charName - ); - - return { optionChar, userStatistique }; -} diff --git a/src/utils/find.ts b/src/utils/find.ts deleted file mode 100644 index 7d63229e..00000000 --- a/src/utils/find.ts +++ /dev/null @@ -1,218 +0,0 @@ -import type { - CharDataWithName, - CharacterData, - PersonnageIds, -} from "@interfaces/database"; -import type { DiscordTextChannel, Settings, Translation } from "@interfaces/discord"; -import type { EClient } from "@main"; - -import { - embedError, - haveAccess, - reply, - searchUserChannel, - sendLogs, - setTagsForRoll, -} from "@utils"; -import * as Djs from "discord.js"; - -export async function isUserNameOrId( - userId: string, - interaction: Djs.ModalSubmitInteraction -) { - if (!userId.match(/\d+/)) - return (await interaction.guild!.members.fetch({ query: userId })).first(); - return await interaction.guild!.members.fetch({ user: userId }); -} - -export async function findMessageBefore( - channel: DiscordTextChannel, - inter: Djs.Message | Djs.InteractionResponse, - client: Djs.Client -) { - let messagesBefore = await channel.messages.fetch({ before: inter.id, limit: 1 }); - let messageBefore = messagesBefore.first(); - while (messageBefore && messageBefore.author.username === client.user?.username) { - messagesBefore = await channel.messages.fetch({ - before: messageBefore.id, - limit: 1, - }); - messageBefore = messagesBefore.first(); - } - return messageBefore; -} - -export async function findLocation( - userData: CharacterData, - interaction: Djs.CommandInteraction, - client: EClient, - ul: Translation, - charData: CharDataWithName, - user?: Djs.User | null -): Promise<{ - thread?: - | Djs.PrivateThreadChannel - | Djs.TextChannel - | Djs.NewsChannel - | Djs.PublicThreadChannel; - sheetLocation: PersonnageIds; -}> { - const sheetLocation: PersonnageIds = { - channelId: userData.messageId[1], - messageId: userData.messageId[0], - }; - const thread = await searchUserChannel( - client.settings, - interaction, - ul, - sheetLocation?.channelId - ); - if (!thread) { - await reply(interaction, { embeds: [embedError(ul("error.noThread"), ul)] }); - return { sheetLocation }; - } - const allowHidden = haveAccess(interaction, thread.id, user?.id ?? interaction.user.id); - if (!allowHidden && charData[user?.id ?? interaction.user.id]?.isPrivate) { - await reply(interaction, { embeds: [embedError(ul("error.private"), ul)] }); - return { sheetLocation }; - } - return { thread, sheetLocation }; -} - -/** - * Find a thread by their data or create it for roll - */ -export async function findThread( - db: Settings, - channel: Djs.TextChannel, - ul: Translation, - hidden?: string -) { - const guild = channel.guild.id; - const rollChannelId = !hidden ? db.get(guild, "rollChannel") : hidden; - if (rollChannelId) { - try { - const rollChannel = await channel.guild.channels.fetch(rollChannelId); - // noinspection SuspiciousTypeOfGuard - if ( - rollChannel instanceof Djs.ThreadChannel || - rollChannel instanceof Djs.TextChannel - ) { - return rollChannel; - } - } catch (e) { - let command = `${ul("config.name")} ${ul("changeThread.name")}`; - - if (hidden) { - db.delete(guild, "hiddenRoll"); - command = `${ul("config.name")} ${ul("hidden.title")}`; - } else db.delete(guild, "rollChannel"); - await sendLogs(ul("error.rollChannelNotFound", { command }), channel.guild, db); - } - } - await channel.threads.fetch(); - await channel.threads.fetchArchived(); - const mostRecentThread = channel.threads.cache.sort((a, b) => { - const aDate = a.createdTimestamp; - const bDate = b.createdTimestamp; - if (aDate && bDate) { - return bDate - aDate; - } - return 0; - }); - const threadName = `🎲 ${channel.name.replaceAll("-", " ")}`; - const thread = mostRecentThread.find( - (thread) => thread.name.startsWith("🎲") && !thread.archived - ); - if (thread) { - const threadThatMustBeArchived = mostRecentThread.filter( - (tr) => tr.name.startsWith("🎲") && !tr.archived && tr.id !== thread.id - ); - for (const thread of threadThatMustBeArchived) { - await thread[1].setArchived(true); - } - return thread; - } - if (mostRecentThread.find((thread) => thread.name === threadName && thread.archived)) { - const thread = mostRecentThread.find( - (thread) => thread.name === threadName && thread.archived - ); - if (thread) { - await thread.setArchived(false); - return thread; - } - } - //create thread - const newThread = await channel.threads.create({ - name: threadName, - reason: ul("roll.reason"), - }); - //delete the message about thread creation - await channel.lastMessage?.delete(); - return newThread; -} - -/** - * Find a forum channel already existing or creat it - */ -export async function findForumChannel( - forum: Djs.ForumChannel, - thread: Djs.ThreadChannel | Djs.TextChannel, - db: Settings, - ul: Translation, - hidden?: string -) { - const guild = forum.guild.id; - const rollChannelId = !hidden ? db.get(guild, "rollChannel") : hidden; - if (rollChannelId) { - try { - const rollChannel = await forum.guild.channels.fetch(rollChannelId); - if ( - rollChannel instanceof Djs.ThreadChannel || - rollChannel instanceof Djs.TextChannel - ) { - return rollChannel; - } - } catch (e) { - let command = `${ul("config.name")} ${ul("changeThread.name")}`; - - if (hidden) { - db.delete(guild, "hiddenRoll"); - command = `${ul("config.name")} ${ul("hidden.title")}`; - } else db.delete(guild, "rollChannel"); - await sendLogs(ul("error.rollChannelNotFound", { command }), forum.guild, db); - } - } - const allForumChannel = forum.threads.cache.sort((a, b) => { - const aDate = a.createdTimestamp; - const bDate = b.createdTimestamp; - if (aDate && bDate) { - return bDate - aDate; - } - return 0; - }); - const topic = thread.name; - const rollTopic = allForumChannel.find((thread) => thread.name === `🎲 ${topic}`); - const tags = await setTagsForRoll(forum); - if (rollTopic) { - //archive all other roll topic - if (rollTopic.archived) rollTopic.setArchived(false); - await rollTopic.setAppliedTags([tags.id as string]); - return rollTopic; - } - //create new forum thread - return await forum.threads.create({ - name: `🎲 ${topic}`, - message: { content: ul("roll.reason") }, - appliedTags: [tags.id as string], - }); -} - -export async function findChara(charData: CharDataWithName, charName?: string) { - return Object.values(charData).find((data) => { - if (data.charName && charName) { - return data.charName.subText(charName); - } - return data.charName === charName; - }); -} diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index 645383c2..00000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,482 +0,0 @@ -// noinspection SuspiciousTypeOfGuard - -import { TUTORIAL_IMAGES } from "@interfaces/constant"; -import type { UserData, UserRegistration } from "@interfaces/database"; -import type { DiscordChannel, Settings, Translation } from "@interfaces/discord"; -import { logger } from "@logger"; -import { editUserButtons, selectEditMenu } from "@utils/buttons"; -import { registerUser, setDefaultManagerId } from "@utils/db"; -import { parseEmbedFields } from "@utils/parse"; -import * as Djs from "discord.js"; -import moment from "moment"; - -/** - * Deletes a given message after a specified time delay. - * If the time delay is zero, the function exits immediately. - * Uses setTimeout to schedule the deletion and handles any errors silently. - * @param message - An instance of InteractionResponse or Message that needs to be deleted. - * @param time - A number representing the delay in milliseconds before the message is deleted. - */ -export async function deleteAfter( - message: Djs.InteractionResponse | Djs.Message, - time: number -): Promise { - if (time === 0) return; - - setTimeout(async () => { - try { - await message.delete(); - } catch (error) { - // Can't delete message, probably because the message was already deleted; ignoring the error. - } - }, time); -} -/** - * Set the tags for thread channel in forum - */ -export async function setTagsForRoll(forum: Djs.ForumChannel) { - //check if the tags `🪡 roll logs` exists - const allTags = forum.availableTags; - const diceRollTag = allTags.find( - (tag) => tag.name === "Dice Roll" && tag.emoji?.name === "🪡" - ); - if (diceRollTag) return diceRollTag; - - const availableTags: Djs.GuildForumTagData[] = allTags.map((tag) => { - return { - id: tag.id, - moderated: tag.moderated, - name: tag.name, - emoji: tag.emoji, - }; - }); - availableTags.push({ - name: "Dice Roll", - emoji: { id: null, name: "🪡" }, - }); - await forum.setAvailableTags(availableTags); - - return forum.availableTags.find( - (tag) => tag.name === "Dice Roll" && tag.emoji?.name === "🪡" - ) as Djs.GuildForumTagData; -} - -/** - * Repost the character sheet in the thread / channel selected with `guildData.managerId` - */ -export async function repostInThread( - embed: Djs.EmbedBuilder[], - interaction: Djs.BaseInteraction, - userTemplate: UserData, - userId: string, - ul: Translation, - which: { stats?: boolean; dice?: boolean; template?: boolean }, - guildData: Settings, - threadId: string -) { - userTemplate.userName = userTemplate.userName - ? userTemplate.userName.toLowerCase() - : undefined; - const damageName = userTemplate.damage ? Object.keys(userTemplate.damage) : undefined; - const channel = interaction.channel; - // noinspection SuspiciousTypeOfGuard - if (!channel || channel instanceof Djs.CategoryChannel) return; - if (!guildData) - throw new Error( - ul("error.generic.e", { - e: "No server data found in database for this server.", - }) - ); - const dataToSend = { - embeds: embed, - components: [editUserButtons(ul, which.stats, which.dice), selectEditMenu(ul)], - }; - let isForumThread = false; - let thread = await searchUserChannel(guildData, interaction, ul, threadId, true); - let msg: Djs.Message | undefined = undefined; - if (!thread) { - const channel = await interaction.guild?.channels.fetch(threadId); - // noinspection SuspiciousTypeOfGuard - if (channel && channel instanceof Djs.ForumChannel) { - const userName = - userTemplate.userName ?? - (await interaction.guild?.members.fetch(userId))?.displayName; - //create a new thread in the forum - const newThread = await channel.threads.create({ - name: userName ?? `${ul("common.sheet")} ${ul("common.character").toUpperCase()}`, - autoArchiveDuration: Djs.ThreadAutoArchiveDuration.OneWeek, - message: dataToSend, - }); - thread = newThread as Djs.AnyThreadChannel; - isForumThread = true; - const starterMsg = await newThread.fetchStarterMessage(); - if (!starterMsg) throw new Error(ul("error.noThread")); - msg = starterMsg; - const ping = await thread.send( - interaction.user.id !== userId - ? `<@${interaction.user.id}> || <@${userId}>` - : `<@${interaction.user.id}>` - ); - await deleteAfter(ping, 5000); - } - } else { - // noinspection SuspiciousTypeOfGuard - if (!thread && channel instanceof Djs.TextChannel) - thread = await createDefaultThread(channel, guildData, interaction); - } - if (!thread) { - throw new Error(ul("error.noThread")); - } - if (!isForumThread) msg = await thread.send(dataToSend); - if (!msg) throw new Error(ul("error.noThread")); - const userRegister: UserRegistration = { - userID: userId, - isPrivate: userTemplate.private, - charName: userTemplate.userName, - damage: damageName, - msgId: [msg.id, thread.id], - }; - registerUser(userRegister, interaction, guildData); -} - -export async function createDefaultThread( - parent: Djs.TextChannel, - guildData: Settings, - interaction: Djs.BaseInteraction, - save = true -) { - let thread = (await parent.threads.fetch()).threads.find( - (thread) => thread.name === "📝 • [STATS]" - ) as Djs.AnyThreadChannel | undefined; - if (!thread) { - thread = (await parent.threads.create({ - name: "📝 • [STATS]", - autoArchiveDuration: 10080, - })) as Djs.AnyThreadChannel; - if (save) setDefaultManagerId(guildData, interaction, thread.id); - } - return thread; -} - -/** - * Create a neat timestamp in the discord format - */ -export function timestamp(settings: Settings, guildID: string) { - if (settings.get(guildID, "timestamp")) - return ` • -`; - return ""; -} - -export class NoEmbed extends Error { - constructor() { - super(); - this.name = "NoEmbed"; - } -} - -export class InvalidCsvContent extends Error { - file?: string; - constructor(file?: string) { - super(); - this.name = "InvalidCsvContent"; - this.file = file; - } -} - -export class InvalidURL extends Error { - constructor(url?: string) { - super(url); - this.name = "InvalidURL"; - } -} - -export class NoChannel extends Error { - constructor() { - super(); - this.name = "NoChannel"; - } -} - -/** - * Verify if an array is equal to another - * @param array1 {string[]|undefined} - * @param array2 {string[]|undefined} - */ -export function isArrayEqual(array1: string[] | undefined, array2: string[] | undefined) { - if (!array1 || !array2) return false; - return ( - array1.length === array2.length && - array1.every((value, index) => value === array2[index]) - ); -} - -/** - * Escape regex string - * @param string {string} - */ -export function escapeRegex(string: string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -/** - * filter the choices by removing the accents and check if it includes the removedAccents focused - * @param choices {string[]} - * @param focused {string} - */ -export function filterChoices(choices: string[], focused: string) { - //remove duplicate from choices, without using set - const values = uniqueValues(choices).filter((choice) => - choice.subText(focused.removeAccents()) - ); - if (values.length >= 25) return values.slice(0, 25); - return values; -} - -export function uniqueValues(array: string[]) { - const seen: { [key: string]: boolean } = {}; - const uniqueArray: string[] = []; - - for (const item of array) { - const formattedItem = item.standardize(); - if (!seen[formattedItem]) { - seen[formattedItem] = true; - uniqueArray.push(item); - } - } - return uniqueArray; -} - -/** - * Parse the fields in stats, used to fix combinaison and get only them and not their result - */ -export function parseStatsString(statsEmbed: Djs.EmbedBuilder) { - const stats = parseEmbedFields(statsEmbed.toJSON() as Djs.Embed); - const parsedStats: { [name: string]: number } = {}; - for (const [name, value] of Object.entries(stats)) { - let number = Number.parseInt(value, 10); - if (Number.isNaN(number)) { - const combinaison = value.replace(/`(.*)` =/, "").trim(); - number = Number.parseInt(combinaison, 10); - } - parsedStats[name] = number; - } - return parsedStats; -} - -export async function sendLogs(message: string, guild: Djs.Guild, db: Settings) { - const guildData = db.get(guild.id); - if (!guildData?.logs) return; - const channel = guildData.logs; - try { - const channelToSend = (await guild.channels.fetch(channel)) as Djs.TextChannel; - await channelToSend.send(message); - } catch (error) { - return; - } -} - -export function displayOldAndNewStats( - oldStats?: Djs.APIEmbedField[], - newStats?: Djs.APIEmbedField[] -) { - let stats = ""; - if (oldStats && newStats) { - for (const field of oldStats) { - const name = field.name.toLowerCase(); - const newField = newStats.find((f) => f.name.toLowerCase() === name); - if (!newField) { - stats += `- ~~${field.name}: ${field.value}~~\n`; - continue; - } - if (field.value === newField.value) continue; - stats += `- ${field.name}: ${field.value} ⇒ ${newField.value}\n`; - } - //verify if there is new stats - for (const field of newStats) { - const name = field.name.toLowerCase(); - if (!oldStats.find((f) => f.name.toLowerCase() === name)) { - stats += `- ${field.name}: 0 ⇒ ${field.value}\n`; - } - } - } - return stats; -} - -export async function searchUserChannel( - guildData: Settings, - interaction: Djs.BaseInteraction, - ul: Translation, - channelId: string, - register?: boolean -): Promise { - let thread: Djs.TextChannel | Djs.AnyThreadChannel | undefined | Djs.GuildBasedChannel = - undefined; - try { - const channel = await interaction.guild?.channels.fetch(channelId); - if (channel instanceof Djs.ForumChannel && register) return; - if ( - !channel || - channel instanceof Djs.CategoryChannel || - channel instanceof Djs.ForumChannel || - channel instanceof Djs.MediaChannel || - channel instanceof Djs.StageChannel || - channel instanceof Djs.VoiceChannel - ) { - if ( - interaction instanceof Djs.CommandInteraction || - interaction instanceof Djs.ButtonInteraction || - interaction instanceof Djs.ModalSubmitInteraction - ) - await interaction?.channel?.send({ - embeds: [embedError(ul("error.noThread"), ul)], - }); - - await sendLogs(ul("error.noThread"), interaction.guild as Djs.Guild, guildData); - return; - } - thread = channel; - } catch (error) { - console.error("Error while fetching channel", error); - return; - } - if (!thread) { - if ( - interaction instanceof Djs.CommandInteraction || - interaction instanceof Djs.ButtonInteraction || - interaction instanceof Djs.ModalSubmitInteraction - ) { - if (interaction.replied) - await interaction.editReply({ embeds: [embedError(ul("error.noThread"), ul)] }); - else await reply(interaction, { embeds: [embedError(ul("error.noThread"), ul)] }); - } else - await sendLogs(ul("error.noThread"), interaction.guild as Djs.Guild, guildData); - return; - } - if (thread.isThread() && thread.archived) thread.setArchived(false); - return thread; -} - -export async function downloadTutorialImages() { - const imageBufferAttachments: Djs.AttachmentBuilder[] = []; - for (const url of TUTORIAL_IMAGES) { - const index = TUTORIAL_IMAGES.indexOf(url); - const newMessageAttachment = new Djs.AttachmentBuilder(url, { - name: `tutorial_${index}.png`, - }); - imageBufferAttachments.push(newMessageAttachment); - } - return imageBufferAttachments; -} - -export async function reply( - interaction: - | Djs.CommandInteraction - | Djs.ModalSubmitInteraction - | Djs.ButtonInteraction - | Djs.StringSelectMenuInteraction, - options: string | Djs.InteractionReplyOptions | Djs.MessagePayload -) { - return interaction.replied || interaction.deferred - ? await interaction.editReply(options) - : await interaction.reply(options); -} - -export const embedError = (error: string, ul: Translation, cause?: string) => { - const embed = new Djs.EmbedBuilder() - .setDescription(error) - .setColor("Red") - .setAuthor({ name: ul("common.error"), iconURL: "https://i.imgur.com/2ulUJCc.png" }) - .setTimestamp(); - if (cause) embed.setFooter({ text: cause }); - return embed; -}; - -async function fetchDiceRole(diceEmbed: boolean, guild: Djs.Guild, role?: string) { - if (!diceEmbed || !role) return; - const diceRole = guild.roles.cache.get(role); - if (!diceRole) return await guild.roles.fetch(role); - return diceRole; -} - -async function fetchStatsRole(statsEmbed: boolean, guild: Djs.Guild, role?: string) { - if (!statsEmbed || !role) return; - const statsRole = guild.roles.cache.get(role); - if (!statsRole) return await guild.roles.fetch(role); - return statsRole; -} - -export async function addAutoRole( - interaction: Djs.BaseInteraction, - member: string, - diceEmbed: boolean, - statsEmbed: boolean, - db: Settings -) { - const autoRole = db.get(interaction.guild!.id, "autoRole"); - if (!autoRole) return; - try { - let guildMember = interaction.guild!.members.cache.get(member); - if (!guildMember) { - //Use the fetch in case the member is not in the cache - guildMember = await interaction.guild!.members.fetch(member); - } - //fetch role - const diceRole = await fetchDiceRole(diceEmbed, interaction.guild!, autoRole.dice); - const statsRole = await fetchStatsRole( - statsEmbed, - interaction.guild!, - autoRole.stats - ); - - if (diceEmbed && diceRole) await guildMember.roles.add(diceRole); - - if (statsEmbed && statsRole) await guildMember.roles.add(statsRole); - } catch (e) { - logger.error("Error while adding role", e); - //delete the role from database so it will be skip next time - db.delete(interaction.guild!.id, "autoRole"); - const dbLogs = db.get(interaction.guild!.id, "logs"); - const errorMessage = `\`\`\`\n${(e as Error).message}\n\`\`\``; - if (dbLogs) { - const logs = await interaction.guild!.channels.fetch(dbLogs); - if (logs instanceof Djs.TextChannel) { - logs.send(errorMessage); - } - } else { - //Dm the server owner because it's pretty important to know - const owner = await interaction.guild!.fetchOwner(); - owner.send(errorMessage); - } - } -} - -/** - * Check if the user have access to the channel where the data is stored - * - It always return true: - * - if the user is the owner of the data - * - if the user have the permission to manage roles - * - It returns false: - * - If there is no user or member found - * - If the thread doesn't exist (data will be not found anyway) - * - * It will ultimately check if the user have access to the channel (with reading permission) - * @param interaction {Djs.BaseInteraction} - * @param thread {Djs.GuildChannelResolvable} if undefined, return false (because it's probably that the channel doesn't exist anymore, so we don't care about it) - * @param user {User | null} if null, return false - * @returns {boolean} - */ -export function haveAccess( - interaction: Djs.BaseInteraction, - thread: Djs.GuildChannelResolvable, - user?: string -): boolean { - if (!user) return false; - if (user === interaction.user.id) return true; - //verify if the user have access to the channel/thread, like reading the channel - const member = interaction.guild?.members.cache.get(interaction.user.id); - if (!member || !thread) return false; - return ( - member.permissions.has(Djs.PermissionFlagsBits.ManageRoles) || - member.permissionsIn(thread).has(Djs.PermissionFlagsBits.ViewChannel) - ); -} diff --git a/template/dnd_test.json b/template/dnd_test.json deleted file mode 100644 index 44630661..00000000 --- a/template/dnd_test.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "charName": true, - "statistics": { - "Force": { - "min": 1 - }, - "Dextérité": { - "min": 1 - }, - "Constitution": { - "min": 1 - }, - "Intelligence": { - "min": 1 - }, - "Sagesse": { - "min": 1 - }, - "Charisme": { - "min": 1 - } - }, - "diceType": "1d20+{{ceil(($-10)/2)}}>=20", - "critical": { - "failure": 1, - "success": 20 - }, - "total": 27 -} diff --git a/template/simple_real_example.json b/template/simple_real_example.json deleted file mode 100644 index 952c8be9..00000000 --- a/template/simple_real_example.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "charName": false, - "statistics": { - "Force": { - "min": 3 - }, - "Endurance": { - "min": 3 - }, - "Agilité": { - "min": 3 - }, - "Constitution": { - "min": 3 - }, - "Éducation": { - "min": 3 - }, - "Intelligence": { - "min": 3 - }, - "Charisme": { - "min": 3 - }, - "Pouvoir": { - "min": 3 - }, - "PV": { - "combinaison": "endurance*2+force" - } - }, - "diceType": "1d20+$>=20", - "critical": { - "failure": 1, - "success": 50 - }, - "total": 88 -} diff --git a/template/test_combinaison.json b/template/test_combinaison.json deleted file mode 100644 index d2989e6c..00000000 --- a/template/test_combinaison.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "charName": false, - "statistics": { - "Force": { - "min": 1 - }, - "Endurance": { - "min": 1 - }, - "Agilité": { - "min": 1 - }, - "logique": { - "combinaison": "force+endurance" - } - }, - "diceType": "1d20>$", - "total": 88 -} diff --git a/tests/bulk_add.test.ts b/tests/bulk_add.test.ts deleted file mode 100644 index 6bc6f724..00000000 --- a/tests/bulk_add.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; - -import type { UserData } from "src/interfaces/database"; -import { - cloneWithoutUserName, - clonedExpectedResultWithSkills, - expectedResult, - guildTemplate, -} from "./constant"; - -import { parseCSV } from "src/utils/import_csv"; - -describe("parseCSV", () => { - it("should be valid and equal", async () => { - const csv = readFileSync("tests/data/should_pass.csv", "utf-8"); - const { members } = await parseCSV(csv, guildTemplate); - expect(members).toEqual(expectedResult); - }); - it("should throw an error", async () => { - const csv = ""; //empty - await expect(parseCSV(csv, guildTemplate)).rejects.toThrow(); - }); - it("should remove duplicate keys", async () => { - const csv = readFileSync("tests/data/duplicate.csv", "utf-8"); - const { members } = await parseCSV(csv, guildTemplate); - expect(members).toEqual(expectedResult); - }); - it("should skip the empty charname", async () => { - const csv = readFileSync("tests/data/empty_char.csv", "utf-8"); - const { members } = await parseCSV(csv, guildTemplate); - const expectedClean = structuredClone(expectedResult) as { - [id: string]: UserData[]; - }; - expectedClean.truc = []; - expect(members).toEqual(expectedClean); - }); - it("should skip the empty stats", async () => { - const csv = readFileSync("tests/data/empty_stats.csv", "utf-8"); - const { members } = await parseCSV(csv, guildTemplate); - const expectedClean = structuredClone(expectedResult) as { - [id: string]: UserData[]; - }; - expectedClean.truc = []; - expect(members).toEqual(expectedClean); - }); - it("should ignore the added columns", async () => { - const csv = readFileSync("tests/data/added_columns.csv", "utf-8"); - const { members } = await parseCSV(csv, guildTemplate); - expect(members).toEqual(expectedResult); - }); - it("should throw an error because of missing header", async () => { - const csv = readFileSync("tests/data/wrong_header.csv", "utf-8"); - await expect(parseCSV(csv, guildTemplate)).rejects.toThrow("Missing header values"); - }); - it("should pass the quoted as it was a normal value", async () => { - const csv = readFileSync("tests/data/quoted.csv", "utf-8"); - const { members } = await parseCSV(csv, guildTemplate); - const expected = structuredClone(expectedResult) as { - [id: string]: UserData[]; - }; - expected["12548784545"] = [ - { - template: expected.truc[0].template, - stats: expected.truc[0].stats, - userName: "helo", - }, - ]; - expect(members).toEqual(expected); - }); -}); - -describe("parseCSV with no userName", () => { - it("should pass", async () => { - const csv = readFileSync("tests/data/should_pass.csv", "utf-8"); - const { members } = await parseCSV(csv, cloneWithoutUserName); - expect(members).toEqual(expectedResult); - }); - it("should pass even with empty charname", async () => { - const csv = readFileSync("tests/data/noUserName/should_pass.csv", "utf-8"); - const { members } = await parseCSV(csv, cloneWithoutUserName); - const expected = structuredClone(expectedResult) as { - [id: string]: UserData[]; - }; - expected.truc[0].userName = null; - expect(members).toEqual(expected); - }); - it("should allow isPrivate", async () => { - const csv = readFileSync("tests/data/noUserName/private.csv", "utf-8"); - const { members } = await parseCSV(csv, cloneWithoutUserName, undefined, true); - const expected = structuredClone(expectedResult) as { - [id: string]: UserData[]; - }; - expected.truc[0].userName = null; - expected.truc[0].private = undefined; - expected.mara__li[0].private = true; - expect(members).toEqual(expected); - }); -}); - -describe("parseCSV with skills", () => { - it("should pass", async () => { - const csv = readFileSync("tests/data/skills/should_pass.csv", "utf-8"); - const { members } = await parseCSV(csv, guildTemplate); - expect(members).toEqual(clonedExpectedResultWithSkills); - }); -}); diff --git a/tests/constant.ts b/tests/constant.ts deleted file mode 100644 index 79e212f3..00000000 --- a/tests/constant.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { StatisticalTemplate } from "@dicelette/core"; - -import type { UserData } from "src/interfaces/database"; - -export const guildTemplate: StatisticalTemplate = { - charName: true, - statistics: { - STR: { max: 18, min: 3 }, - DEX: { max: 18, min: 3 }, - CON: { max: 18, min: 3 }, - INT: { max: 18, min: 3 }, - WIS: { max: 18, min: 3 }, - CHA: { max: 18, min: 3 }, - }, - total: 88, - diceType: "4d6", - critical: { success: 20, failure: 1 }, - damage: { - STR: "1d4", - DEX: "1d4", - CON: "1d4", - INT: "1d4", - WIS: "1d4", - CHA: "1d4", - }, -}; - -export const cloneWithoutUserName: StatisticalTemplate = structuredClone( - structuredClone(guildTemplate) -); -cloneWithoutUserName.charName = false; - -const temp = { - diceType: "4d6", - critical: { success: 20, failure: 1 }, -}; - -export const expectedResult: { [id: string]: UserData[] } = { - mara__li: [ - { - userName: "Blaïka", - stats: { - STR: 12, - DEX: 12, - CON: 12, - INT: 12, - WIS: 12, - CHA: 12, - }, - template: temp, - channel: undefined, - }, - ], - truc: [ - { - userName: "machin", - stats: { - STR: 11, - DEX: 10, - CON: 11, - INT: 10, - WIS: 11, - CHA: 10, - }, - template: temp, - channel: undefined, - }, - ], -}; - -export const clonedExpectedResultWithSkills: { [id: string]: UserData[] } = - structuredClone(expectedResult); - -clonedExpectedResultWithSkills.mara__li[0].damage = { - Athletics: "1d4+STR", - Acrobatics: "1d4+DEX", -}; - -clonedExpectedResultWithSkills.truc[0].damage = { - Athletics: "1d4+STR", - Acrobatics: "1d4+DEX", -}; diff --git a/tests/data/added_columns.csv b/tests/data/added_columns.csv deleted file mode 100644 index b5b4f171..00000000 --- a/tests/data/added_columns.csv +++ /dev/null @@ -1,3 +0,0 @@ -user;charName;STR;DEX;CON;INT;WIS;CHA;Other; -mara__li;Blaïka;12;12;12;12;12;12;12; -truc;machin;11;10;11;10;11;10;10; diff --git a/tests/data/duplicate.csv b/tests/data/duplicate.csv deleted file mode 100644 index 459dcf34..00000000 --- a/tests/data/duplicate.csv +++ /dev/null @@ -1,6 +0,0 @@ -user;charName;STR;DEX;CON;INT;WIS;CHA -mara__li;Blaïka;12;12;12;12;12;12 -truc;machin;11;10;11;10;11;10 -mara__li;Blaïka; 12;12;12;12;12; 12 -truc;machin;11;10;11;10;11; 10 - diff --git a/tests/data/empty_char.csv b/tests/data/empty_char.csv deleted file mode 100644 index 30f7cbe5..00000000 --- a/tests/data/empty_char.csv +++ /dev/null @@ -1,3 +0,0 @@ -user ;charName ;STR ;DEX ;CON ;INT ;WIS ;CHA -mara__li ;Blaïka ; 12 ; 12 ; 12 ; 12 ; 12 ; 12 -truc ; ; 11 ; 10 ; 11 ; 10 ; 11 ; 10 diff --git a/tests/data/empty_stats.csv b/tests/data/empty_stats.csv deleted file mode 100644 index ce5ece3d..00000000 --- a/tests/data/empty_stats.csv +++ /dev/null @@ -1,3 +0,0 @@ -user ;charName ;STR ;DEX ;CON ;INT ;WIS ;CHA -mara__li ;Blaïka ; 12 ; 12 ; 12 ; 12 ; 12 ; 12 -truc ;machin ; 11 ; 10 ; 11 ; 10 ; ; diff --git a/tests/data/noUserName/private.csv b/tests/data/noUserName/private.csv deleted file mode 100644 index 367ba313..00000000 --- a/tests/data/noUserName/private.csv +++ /dev/null @@ -1,3 +0,0 @@ -user;charName;STR;DEX;CON;INT;WIS;CHA;isPrivate -mara__li;Blaïka;12;12;12;12;12;12;true -truc;;11;10;11;10;11;10;false diff --git a/tests/data/noUserName/should_pass.csv b/tests/data/noUserName/should_pass.csv deleted file mode 100644 index ee2e7964..00000000 --- a/tests/data/noUserName/should_pass.csv +++ /dev/null @@ -1,3 +0,0 @@ -user;charName;STR;DEX;CON;INT;WIS;CHA -mara__li;Blaïka;12;12;12;12;12;12 -truc;;11;10;11;10;11;10 diff --git a/tests/data/quoted.csv b/tests/data/quoted.csv deleted file mode 100644 index f3068b45..00000000 --- a/tests/data/quoted.csv +++ /dev/null @@ -1,4 +0,0 @@ -"user";charName;STR;DEX;CON;INT;WIS;CHA -"mara__li";Blaïka;12;12;12;12;12;12 -"truc";machin;11;10;11;10;11;10 -"12548784545";helo;11;10;11;10;11;10 diff --git a/tests/data/should_pass.csv b/tests/data/should_pass.csv deleted file mode 100644 index 8bbf4df4..00000000 --- a/tests/data/should_pass.csv +++ /dev/null @@ -1,3 +0,0 @@ -user ;charName ;STR ;DEX ;CON ;INT ;WIS ;CHA -mara__li ;Blaïka ; 12 ; 12 ; 12 ; 12 ; 12 ; 12 -truc ;machin ; 11 ; 10 ; 11 ; 10 ; 11 ; 10 diff --git a/tests/data/skills/should_pass.csv b/tests/data/skills/should_pass.csv deleted file mode 100644 index 712a582d..00000000 --- a/tests/data/skills/should_pass.csv +++ /dev/null @@ -1,5 +0,0 @@ -user ;charName ;STR ;DEX ;CON ;INT ;WIS ;CHA;dice -mara__li ;Blaïka ;12;12;12;12;12;12;"- Athletics: 1d4+STR -- Acrobatics: 1d4+DEX" -truc ;machin ;11;10;11;10;11;10;"- Athletics: 1d4+STR -- Acrobatics: 1d4+DEX" diff --git a/tests/data/wrong_header.csv b/tests/data/wrong_header.csv deleted file mode 100644 index 39e45687..00000000 --- a/tests/data/wrong_header.csv +++ /dev/null @@ -1,5 +0,0 @@ -charName;STR;DEX;CON;INT;WIS;CHA -Blaïka;12;12;12;12;12;12 -machin;11;10;11;10;11;10 - - diff --git a/tsconfig.dev.json b/tsconfig.dev.json new file mode 100644 index 00000000..8795afec --- /dev/null +++ b/tsconfig.dev.json @@ -0,0 +1,50 @@ +{ + "ts-node": { + "require": ["tsconfig-paths/register"], + "esm": true, + "transpileOnly": true, + "experimentalSpecifierResolution": "node" + }, + "tsc-alias": { + "resolveFullPaths": true + }, + "compilerOptions": { + "target": "ESNext", + "module": "esnext", + "lib": ["ESNext"], + "rootDir": "./", + "outDir": "./dist/", + "strict": true, + "moduleResolution": "node", + "importHelpers": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "inlineSourceMap": true, + "allowJs": true, + "checkJs": false, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "removeComments": true, + "typeRoots": ["packages/@types", "node_modules/@types"], + "sourceMap": false, + "composite": true, + "baseUrl": ".", + "declaration": true, + "declarationMap": false, + "paths": { + "@dicelette/*": ["packages/*/index.js"], + "client": ["packages/bot/src/client.js"], + "messages": ["packages/bot/src/messages/index.js"], + "utils": ["packages/bot/src/utils/index.js"], + "database": ["packages/bot/src/database/index.js"], + "features": ["packages/bot/src/features/index.js"], + "commands": ["packages/bot/src/commands/index.js"], + "event": ["packages/bot/src/events/index.js"], + "locales": ["packages/bot/src/locales.js"] + } + }, + "exclude": ["node_modules"], + "include": ["packages/**/*.ts", "packages/**/*.d.ts", "package.json"] +} diff --git a/tsconfig.json b/tsconfig.json index 07fab698..92ca2d73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,9 @@ { "ts-node": { - "require": ["tsconfig-paths/register"] + "require": ["tsconfig-paths/register"], + "esm": true, + "transpileOnly": true, + "experimentalSpecifierResolution": "node" }, "tsc-alias": { "resolveFullPaths": true @@ -12,7 +15,7 @@ "rootDir": "./", "outDir": "./dist/", "strict": true, - "moduleResolution": "bundler", + "moduleResolution": "node", "importHelpers": true, "experimentalDecorators": true, "esModuleInterop": true, @@ -24,32 +27,23 @@ "resolveJsonModule": true, "forceConsistentCasingInFileNames": true, "removeComments": true, - "typeRoots": ["src/@types", "node_modules/@types"], + "typeRoots": ["packages/@types", "node_modules/@types"], "sourceMap": false, + "composite": false, "baseUrl": ".", + "declaration": false, + "declarationMap": false, "paths": { - "@utils/*": ["src/utils/*"], - "@utils": ["src/utils/index.js"], - "@interactions/*": ["src/interactions/*"], - "@interactions": ["src/interactions/index.js"], - "@register/*": ["src/interactions/register/*"], - "@localization/*": ["src/localizations/*"], - "@localization": ["src/localizations/index.js"], - "@main": ["src/index.js"], - "@interfaces/*": ["src/interfaces/*"], - "@events/*": ["src/events/*"], - "@events": ["src/events/index.js"], - "@commands/*": ["src/commands/*"], - "@commands": ["src/commands/index.js"], - "@logger": ["src/logger.js"] + "client": ["packages/bot/src/client.js"], + "messages": ["packages/bot/src/messages/index.js"], + "utils": ["packages/bot/src/utils/index.js"], + "database": ["packages/bot/src/database/index.js"], + "features": ["packages/bot/src/features/index.js"], + "commands": ["packages/bot/src/commands/index.js"], + "event": ["packages/bot/src/events/index.js"], + "locales": ["packages/bot/src/locales.js"] } }, - "files": ["src/index.ts"], - "include": ["src/**/*.ts", "src/@types/**/*.d.ts", "jest.config.ts"], - "exclude": [ - "dist", - "node_modules", - "src/uniformize/tests", - "src/uniformize/tsup.config.ts" - ] + "exclude": ["node_modules", "dist"], + "include": ["packages/**/*.ts", "packages/**/*.d.ts", "package.json"] } diff --git a/vitest.config.ts b/vitest.config.ts index af2d3d3a..c149acde 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,16 @@ -import tsconfigPaths from "vite-tsconfig-paths"; +import path from "node:path"; import { defineConfig } from "vitest/config"; export default defineConfig({ - plugins: [tsconfigPaths()], + test: { + exclude: [], + alias: { + "@dicelette/localization": path.resolve( + __dirname, + "packages/localization/index.ts" + ), + "@dicelette/utils": path.resolve(__dirname, "packages/utils/index.ts"), + "@dicelette/types": path.resolve(__dirname, "packages/types/index.ts"), + }, + }, }); diff --git a/vitest.workspace.json b/vitest.workspace.json new file mode 100644 index 00000000..6ad17000 --- /dev/null +++ b/vitest.workspace.json @@ -0,0 +1 @@ +["packages/*"]