From 6d952f84139af8c52569fdd44ae07c19cb6c4154 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:55:08 +0200 Subject: [PATCH 01/67] The beginning --- .data/.gitkeep | 0 .env.example | 5 - .eslintignore | 11 - .eslintrc.json | 28 - .gitignore | 16 +- .prettierignore | 12 - .prettierrc | 9 - .typesafe-i18n.json | 8 + .vscode/settings.json | 3 + LICENSE.md | 157 - README.md | 46 - biome.json | 54 + bun.lock | 309 ++ config/config.example.jsonc | 109 - docker-compose.yml | 28 - docker_run.sh | 19 - dockerfile | 30 - drizzle.config.ts | 13 + locales/cs.json | 70 - locales/de.json | 73 - locales/el.json | 73 - locales/es.json | 70 - locales/fr.json | 71 - locales/id.json | 74 - locales/it.json | 73 - locales/main.json | 74 - locales/my.json | 74 - locales/ru.json | 73 - locales/tr.json | 70 - locales/uk.json | 73 - locales/vi.json | 73 - package-lock.json | 6326 ------------------------------- package.json | 84 +- prisma/compatible.sql | 43 - prisma/docker.prisma | 31 - prisma/postgre.sql | 55 - prisma/schema.prisma | 30 - src/commands/add.ts | 81 - src/commands/claim.ts | 29 - src/commands/clearDM.ts | 34 - src/commands/close.ts | 56 - src/commands/index.ts | 17 - src/commands/massadd.ts | 89 - src/commands/remove.ts | 62 - src/commands/rename.ts | 50 - src/config/index.ts | 44 + src/db/schema.ts | 27 + src/events/index.ts | 7 - src/events/interactionCreate.ts | 273 -- src/events/ready.ts | 307 -- src/index.ts | 81 +- src/structure/BaseCommand.ts | 13 - src/structure/BaseEvent.ts | 13 - src/structure/ExtendedClient.ts | 85 - src/structure/index.ts | 10 - src/structure/types.ts | 166 - src/types/config.d.ts | 0 src/types/index.d.ts | 1 + src/types/process.d.ts | 10 + src/utils/claim.ts | 128 - src/utils/close.ts | 307 -- src/utils/close_askReason.ts | 52 - src/utils/createTicket.ts | 232 -- src/utils/delete.ts | 46 - src/utils/logs.ts | 167 - src/utils/translation.ts | 134 - tsconfig.json | 53 +- 67 files changed, 534 insertions(+), 10407 deletions(-) create mode 100644 .data/.gitkeep delete mode 100644 .env.example delete mode 100644 .eslintignore delete mode 100644 .eslintrc.json delete mode 100644 .prettierignore delete mode 100644 .prettierrc create mode 100644 .typesafe-i18n.json create mode 100644 .vscode/settings.json delete mode 100644 LICENSE.md delete mode 100644 README.md create mode 100644 biome.json create mode 100644 bun.lock delete mode 100644 config/config.example.jsonc delete mode 100644 docker-compose.yml delete mode 100644 docker_run.sh delete mode 100644 dockerfile create mode 100644 drizzle.config.ts delete mode 100644 locales/cs.json delete mode 100644 locales/de.json delete mode 100644 locales/el.json delete mode 100644 locales/es.json delete mode 100644 locales/fr.json delete mode 100644 locales/id.json delete mode 100644 locales/it.json delete mode 100644 locales/main.json delete mode 100644 locales/my.json delete mode 100644 locales/ru.json delete mode 100644 locales/tr.json delete mode 100644 locales/uk.json delete mode 100644 locales/vi.json delete mode 100644 package-lock.json delete mode 100644 prisma/compatible.sql delete mode 100644 prisma/docker.prisma delete mode 100644 prisma/postgre.sql delete mode 100644 prisma/schema.prisma delete mode 100644 src/commands/add.ts delete mode 100644 src/commands/claim.ts delete mode 100644 src/commands/clearDM.ts delete mode 100644 src/commands/close.ts delete mode 100644 src/commands/index.ts delete mode 100644 src/commands/massadd.ts delete mode 100644 src/commands/remove.ts delete mode 100644 src/commands/rename.ts create mode 100644 src/config/index.ts create mode 100644 src/db/schema.ts delete mode 100644 src/events/index.ts delete mode 100644 src/events/interactionCreate.ts delete mode 100644 src/events/ready.ts delete mode 100644 src/structure/BaseCommand.ts delete mode 100644 src/structure/BaseEvent.ts delete mode 100644 src/structure/ExtendedClient.ts delete mode 100644 src/structure/index.ts delete mode 100644 src/structure/types.ts create mode 100644 src/types/config.d.ts create mode 100644 src/types/index.d.ts create mode 100644 src/types/process.d.ts delete mode 100644 src/utils/claim.ts delete mode 100644 src/utils/close.ts delete mode 100644 src/utils/close_askReason.ts delete mode 100644 src/utils/createTicket.ts delete mode 100644 src/utils/delete.ts delete mode 100644 src/utils/logs.ts delete mode 100644 src/utils/translation.ts diff --git a/.data/.gitkeep b/.data/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.env.example b/.env.example deleted file mode 100644 index 7cb14b2f..00000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Your discord token -TOKEN="" -# Prisma Database URL (refer to docs for more details) -# Refer to https://www.prisma.io/docs/concepts/database-connectors to use other databases -DATABASE_URL="file:./tixbot.db" \ No newline at end of file diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 73667cb5..00000000 --- a/.eslintignore +++ /dev/null @@ -1,11 +0,0 @@ -.github -node_modules -.gitignore -.git -CODE_OF_CONDUCT.md -CONTRIBUTING.md -README.md -LICENSE -package.json -package-lock.json -dist/ \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 6b9e58f9..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "env": { - "es2021": true, - "node": true - }, - "overrides": [], - "extends": ["eslint:recommended", "prettier", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "no-unused-vars": "warn", - "no-console": "off", - "@typescript-eslint/no-require-imports": "off", - "no-undef": "warn", - "no-constant-condition": "warn", - "indent": ["error", "tab"], - "semi": ["error", "always"], - "quotes": [2, "double"], - "semi-style": ["error", "last"], - "no-process-exit": "off", - "node/no-missing-import": "off", - "no-var-requires": "off" - } -} diff --git a/.gitignore b/.gitignore index 7bb504db..4768ac10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,7 @@ -node_modules -config.jsonc -token.json -dist -.env -*.db +node_modules/ +dist/ +config/.env +config/config.ts -test.sql -__pycache__/ -*.py[cod] -local_settings.py -.venv +.data/*.db \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index c26dc295..00000000 --- a/.prettierignore +++ /dev/null @@ -1,12 +0,0 @@ -.github -node_modules -.gitignore -.git -CODE_OF_CONDUCT.md -CONTRIBUTING.md -README.md -LICENSE -package.json -package-lock.json -.eslintrc.json -.prettierrc \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 67f9148d..00000000 --- a/.prettierrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "semi": true, - "singleQuote": false, - "printWidth": 150, - "tabWidth": 1, - "useTabs": true, - "endOfLine": "lf", - "trailingComma": "none" -} \ No newline at end of file diff --git a/.typesafe-i18n.json b/.typesafe-i18n.json new file mode 100644 index 00000000..fa38040c --- /dev/null +++ b/.typesafe-i18n.json @@ -0,0 +1,8 @@ +{ + "adapters": [], + "$schema": "https://unpkg.com/typesafe-i18n@5.27.1/schema/typesafe-i18n.json", + "baseLocale": "en", + "outputPath": "./i18n", + "esmImports": true, + "banner": "// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten." +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..fab4c7e2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["bunx", "libsql", "typesafe"] +} diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 311e8a4f..00000000 --- a/LICENSE.md +++ /dev/null @@ -1,157 +0,0 @@ -# Creative Commons Attribution 4.0 International - -Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. - -**Using Creative Commons Public Licenses** - -Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. - -* __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). - -* __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). - -## Creative Commons Attribution 4.0 International Public License - -By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. - -### Section 1 – Definitions. - -a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. - -b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. - -c. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. - -d. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. - -e. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. - -f. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. - -g. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. - -h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. - -i. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. - -j. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. - -k. __You__ means the individual or entity exercising the Licensed Rights under this Public License. __Your__ has a corresponding meaning. - -### Section 2 – Scope. - -a. ___License grant.___ - - 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: - - A. reproduce and Share the Licensed Material, in whole or in part; and - - B. produce, reproduce, and Share Adapted Material. - - 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. - - 3. __Term.__ The term of this Public License is specified in Section 6(a). - - 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. - - 5. __Downstream recipients.__ - - A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. - - B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. - - 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). - -b. ___Other rights.___ - - 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. - - 2. Patent and trademark rights are not licensed under this Public License. - - 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. - -### Section 3 – License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the following conditions. - -a. ___Attribution.___ - - 1. If You Share the Licensed Material (including in modified form), You must: - - A. retain the following if it is supplied by the Licensor with the Licensed Material: - - i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); - - ii. a copyright notice; - - iii. a notice that refers to this Public License; - - iv. a notice that refers to the disclaimer of warranties; - - v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; - - B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and - - C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. - - 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. - - 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. - - 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. - -### Section 4 – Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: - -a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; - -b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and - -c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. - -For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. - -### Section 5 – Disclaimer of Warranties and Limitation of Liability. - -a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ - -b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ - -c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. - -### Section 6 – Term and Termination. - -a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. - -b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: - - 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or - - 2. upon express reinstatement by the Licensor. - - For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. - -c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. - -d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. - -### Section 7 – Other Terms and Conditions. - -a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. - -b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. - -### Section 8 – Interpretation. - -a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. - -b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. - -c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. - -d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. - -> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. -> -> Creative Commons may be contacted at creativecommons.org diff --git a/README.md b/README.md deleted file mode 100644 index 9a191f77..00000000 --- a/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Ticket Bot - -Ticket Bot is a open source project of an ticket discord bot using [discord.js](https://discord.js.org) v14 - -![Discord.js ticket bot](https://i.imgur.com/564YXvR.png) -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSayrix%2FTicket-Bot.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FSayrix%2FTicket-Bot?ref=badge_shield) - -## 📄 Documentation - -The documentation is available [here](https://doc.ticket.pm/) - -## ⚠️ Incompatibility -This new source code you're seeing are completely refactored and will be incompatible with the older version. -I recommend you finish up all of your remaining support ticket and start migrating to the newer version. -If you prefer to stay in the older version, here is the doc for the old version: https://doc.ticket.pm/docs/oldDoc/intro - -## 💬 Discord - -You can come on the discord: https://discord.gg/VasYV6MEJy - -## ✨ Contributing - -Contributions are welcome! Please read the [contributing guidelines](https://github.com/Sayrix/Ticket-Bot/blob/main/CONTRIBUTING.md) first. - -## 👨‍💻 Maintainers -Our current project maintainers: -* [Sayrix](https://github.com/Sayrix) -* [小兽兽/zhiyan114](https://github.com/zhiyan114) - -## 💎 Sponsors -Thanks to all our sponsors! 🙏 -You can see all perks here: https://github.com/sponsors/Sayrix -

- - - -

- -## 🎥 Videos -[Tutorial in french + english subtitle](https://youtu.be/24zAFj8w9gE?si=OvikXeNIJglz4FJV) - -## Please leave a ⭐ to help the project! - - -## License -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSayrix%2FTicket-Bot.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FSayrix%2FTicket-Bot?ref=badge_large) diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..c447b897 --- /dev/null +++ b/biome.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useConst": "error", + "noNonNullAssertion": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noAsyncPromiseExecutor": "off", + "noUnknownAtRules": { + "level": "off", + "options": { + "ignore": ["theme"] + } + } + } + }, + "includes": ["**", "!**/node_modules/", "!**/bun.lock"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "indentWidth": 2, + "lineWidth": 130, + "includes": ["**", "!**/node_modules/", "!**/bun.lock"] + }, + "html": { + "formatter": { + "enabled": true + } + }, + "javascript": { + "formatter": { + "trailingCommas": "none" + }, + "globals": ["Bun"] + }, + "css": { + "parser": { + "tailwindDirectives": true + }, + "formatter": { + "enabled": false + } + }, + "files": { + "includes": ["**", "!**/node_modules/", "!**/bun.lock"] + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..586764ba --- /dev/null +++ b/bun.lock @@ -0,0 +1,309 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "dependencies": { + "@libsql/client": "^0.17.2", + "dotenv": "^17.4.1", + "drizzle-orm": "^0.45.2", + "typesafe-i18n": "^5.27.1", + }, + "devDependencies": { + "@biomejs/biome": "^2.4.10", + "@types/bun": "^1.3.11", + "@typescript/native-preview": "^7.0.0-dev.20260409.1", + "drizzle-kit": "^0.31.10", + }, + }, + }, + "packages": { + "@biomejs/biome": ["@biomejs/biome@2.4.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.10", "@biomejs/cli-darwin-x64": "2.4.10", "@biomejs/cli-linux-arm64": "2.4.10", "@biomejs/cli-linux-arm64-musl": "2.4.10", "@biomejs/cli-linux-x64": "2.4.10", "@biomejs/cli-linux-x64-musl": "2.4.10", "@biomejs/cli-win32-arm64": "2.4.10", "@biomejs/cli-win32-x64": "2.4.10" }, "bin": { "biome": "bin/biome" } }, "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.10", "", { "os": "win32", "cpu": "x64" }, "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@libsql/client": ["@libsql/client@0.17.2", "", { "dependencies": { "@libsql/core": "^0.17.2", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q=="], + + "@libsql/core": ["@libsql/core@0.17.2", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-L8qv12HZ/jRBcETVR3rscP0uHNxh+K3EABSde6scCw7zfOdiLqO3MAkJaeE1WovPsjXzsN/JBoZED4+7EZVT3g=="], + + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.29", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A=="], + + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.29", "", { "os": "darwin", "cpu": "x64" }, "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ=="], + + "@libsql/hrana-client": ["@libsql/hrana-client@0.9.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "cross-fetch": "^4.0.0", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw=="], + + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], + + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ=="], + + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.29", "", { "os": "linux", "cpu": "arm" }, "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg=="], + + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w=="], + + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.29", "", { "os": "linux", "cpu": "arm64" }, "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg=="], + + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg=="], + + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.29", "", { "os": "linux", "cpu": "x64" }, "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w=="], + + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.29", "", { "os": "win32", "cpu": "x64" }, "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg=="], + + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + + "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260409.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260409.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260409.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-CV1HEMGo1xCySwUJbCQOF+mmrTue8KTJ1Od2kKWhcbOpu8fPBfaqIpbAM6tGLcNEykEjMMTYHc/VTLbMgxdScQ=="], + + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260409.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GcRRnaoeZVrbC47woQ/2t3vPoQcTSjsWPEAQGtwNSdw7Z9TKxG4ES22ghJIQXd3ncTRCMJ+XELnnuqxVutkJ9w=="], + + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260409.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-7s8DXAa0Xpu/8PEjYIc4I36Ju7eVpoz9k3E+3WQdOF8pIPWYohiOj+zi68m9XYQck+rnkjUFo26ThVKqVetoMA=="], + + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "arm" }, "sha512-fOa07JBUXQpEPq+024g346inYZ2xp63ELuoRq6J0jwDWQ/ftCCuvdQNMncwFhsm1qlMdKT3S68NrnSxX16hiaw=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cGTzTUqRGlIDwdtkDy6qTrvrqpe27W4CdgnFn0FpxpiWnaIi3wqjlzQ1grtqrqainw/yuPy5hn/I86sQgN6nvA=="], + + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "x64" }, "sha512-lQrbc/BJKBxQrR1ttBDU5sYY1Hb2moFQgHL20T6nbapNqGpK4pzy64p+NK39O93D4omiCSk04pkchBCVrMPSAg=="], + + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260409.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-kmCafMo1xZlYx+9WnfpeZJ2tnB/CcJdR8QPX7j9vqcpe51D7b7Intmr921dD48KGpVh5YgjQ1MEFE5mjGqGMaA=="], + + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260409.1", "", { "os": "win32", "cpu": "x64" }, "sha512-WRd+JpQipTsE15QgYr3w7J0f1NKvGcq2QEgmcq8hB0WZA1X2WhQopNu+MpPQ3tdDD42VjMhm8ZoB8HpuOoXK5w=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + + "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + + "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], + + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], + + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + + "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + + "typesafe-i18n": ["typesafe-i18n@5.27.1", "", { "peerDependencies": { "typescript": ">=3.5.1" }, "bin": { "typesafe-i18n": "cli/typesafe-i18n.mjs" } }, "sha512-749uWo2ZXETT//kWjVYPm8QPYR8xLh8G0wLfoAyCAtAmysX67uCaAyLjAjAWojL6fuJpE5B6yIjwvO9orXzUPg=="], + + "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + } +} diff --git a/config/config.example.jsonc b/config/config.example.jsonc deleted file mode 100644 index df913234..00000000 --- a/config/config.example.jsonc +++ /dev/null @@ -1,109 +0,0 @@ -{ - "clientId": "1111111111111111111", // The id of the discord bot - "guildId": "1111111111111111111", // The id of the discord server - "mainColor": "#f6c42f", // The hex color of the embeds by default - "lang": "main", // If you want to set english please set "main" - - "openTicketChannelId": "1111111111111111111", // The id of the channel where the message to create a ticket will be sent - - "ticketTypes": [ - // You have a limit of 25 types (the limit of Discord) - { - "codeName": "category-one", // The name need to be in lowercase - "name": "Category One", // The name that will be displayed in the ticket - "description": "Description of Category One", // The description of the Ticket in Create Ticket Menu - "emoji": "💡", // The emoji of the type (can be blank) - "color": "", // Can be a hex color or blank to use the main color - "categoryId": "1111111111111111111", // The category id where the tickets will be created - "ticketNameOption": "💡ticket-TICKETCOUNT", // Here is all parameter: USERNAME, USERID, TICKETCOUNT (set to blank to use the default name) - "customDescription": "", // The custom description of the ticket type, here is all parameter: USERNAME, USERID, TICKETCOUNT, REASON1, 2, ect (set to blank to use the default description) - "cantAccess": ["1111111111111111111"], // The roles who can't access to this ticket type - "askQuestions": false, // If the bot should ask the reason of the ticket - "questions": [], // Leave blank if you don't want to ask questions - "staffRoles": [] // Category specific staff role (instead of the default ones) - }, - { - "codeName": "category-two", // The name need to be in lowercase - "name": "Category Two", // The name that will be displayed in the ticket - "description": "Description of Category Two", // The description of the Ticket in Create Ticket Menu - "emoji": "🛑", // The emoji of the type (can be blank) - "color": "#f8312f", // Can be a hex color or blank to use the main color - "categoryId": "1111111111111111111", // The category id where the tickets will be created - "ticketNameOption": "", // Here is all parameter: USERNAME, USERID, TICKETCOUNT (set to blank to use the default name) - "customDescription": "Please explain your report in detail. If you have any images, please attach them to your message.", // The custom description of the ticket type, here is all parameter: USERNAME, USERID, TICKETCOUNT, REASON1, 2, ect (set to blank to use the default description) - "cantAccess": ["2222222222222222222"], // The roles who can't access to this ticket type - "askQuestions": false, // If the bot should ask the reason of the ticket - "questions": [], // Leave blank if you don't want to ask questions - "staffRoles": [] // Category specific staff role (instead of the default ones) - }, - { - "codeName": "other", // The name need to be in lowercase - "name": "Other", // The name that will be displayed in the ticket - "description": "Description of Category Other", // The description of the Ticket in Create Ticket Menu - "emoji": "", // The emoji of the type (can be blank) - "color": "", // Can be a hex color or blank to use the main color - "categoryId": "1111111111111111111", // The category id where the tickets will be created - "ticketNameOption": "", // Here is all parameter: USERNAME, USERID, TICKETCOUNT (set to blank to use the default name) - "customDescription": "Thank you for your ticket, a staff will reply you as soon as possible\n\n__**What is the reason of the ticket?**__: REASON1", // The custom description of the ticket type, here is all parameter: USERNAME, USERID, TICKETCOUNT, REASON1, 2, ect (set to blank to use the default description) - "cantAccess": [], // The roles who can't access to this ticket type - "askQuestions": true, // If the bot should ask the reason of the ticket - "staffRoles": [], // Category specific staff role (instead of the default ones) - "questions": [ - // Maximum of 5 questions can be set due to discord's limit - { - "label": "What is the reason of the ticket?", - "placeholder": "Please enter the reason", - "style": "PARAGRAPH", // SHORT or PARAGRAPH - "maxLength": 1000 - } - ] - } - ], - "ticketNameOption": "Ticket-TICKETCOUNT", // Here is all parameter: USERNAME, USERID, TICKETCOUNT - - // Ticket Claim Options - "claimOption": { - "claimButton": true, // Whether to enable ticket claim button or not - // The X can be replaced with S (The staff that claimed the ticket) or U (The user that created the ticket) - "nameWhenClaimed": "✔️ Ticket-TICKETCOUNT", // Here is all parameter: X_USERNAME, X_USERID, TICKETCOUNT - "categoryWhenClaimed": "" // The category the ticket is moved to when claimed - }, - - "rolesWhoHaveAccessToTheTickets": ["1111111111111111111", "2222222222222222222"], // Roles who can access to the tickets (Like the staff)/ Treat this as global admin role type of thing. - - "rolesWhoCanNotCreateTickets": [], // Roles who can not create a tickets (Like a blacklist) - - "pingRoleWhenOpened": true, - "roleToPingWhenOpenedId": ["1111111111111111111"], // The role to ping when a ticket is opened - - "logs": true, - "logsChannelId": "1111111111111111111", // The id of the channel where the logs will be sent - - "closeOption": { - "closeButton": true, // If false the ticket can be closed only by doing /closes - "dmUser": true, // Whether to DM the user when the ticket is closed - "createTranscript": true, // If set to true, when the ticket is closed a transcript will be generated and sent in the logs channel - "askReason": true, // If false the ticket will be closed without asking the reason - "whoCanCloseTicket": "STAFFONLY", // STAFFONLY (roles configured at "rolesWhoHaveAccessToTheTickets") or EVERYONE - "deleteTicket": false, // when enabled, it will delete the ticket on clicking close button - "closeTicketCategoryId": "" // The id of the category where a closed ticket will be moved to. Leave blank to disable this feature - }, - "uuidType": "uuid", // uuid or emoji - - "status": { - "enabled": true, // If you want to enable the status of the bot - "text": "github.com/Sayrix", // The text of the status - "type": "PLAYING", // PLAYING, WATCHING, LISTENING, STREAMING, COMPETING - "url": "https://twitch.tv/grimkujow", // The url of the status if the type is STREAMING (can be blank) - "status": "online" // online, idle, dnd, invisible set to online if the type is STREAMING - }, - - "maxTicketOpened": 0, // The number of tickets the user can open while another one is already open. Set to 0 to unlimited - /* - Whether or not to minimizing the tracking data that are being sent - Enabling this will cause the telemetry to only send the software version and node version - */ - "minimalTracking": false, - // Hide internal websocket logs - "showWSLog": false -} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index c4c965f7..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: '3' - -services: - bot: - build: . - container_name: ticket-bot - environment: - - TOKEN - networks: - - net - depends_on: - - pgsql - restart: on-failure - volumes: - - /opt/ticket-bot/config:/app/config - pgsql: - image: postgres:15.3-alpine - environment: - - POSTGRES_PASSWORD=postgres - networks: - - net - volumes: - - /opt/ticket-bot/pgsql:/var/lib/postgresql/data - restart: on-failure - -networks: - net: - driver: bridge \ No newline at end of file diff --git a/docker_run.sh b/docker_run.sh deleted file mode 100644 index 1544891b..00000000 --- a/docker_run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Setup the config files -if [ ! -f "./config/config.json" ]; then - if [ -f "./temp_config/config.jsonc" ]; then - # Config already setup by the user - echo "Pre-build config detected, moving to config folder..."; - mv ./temp_config/config.jsonc ./config/config.jsonc - else - # Config not setup by the user - echo "Config not detected, creating config..."; - echo "Make sure to edit the config file in /opt/ticket-bot/config/config.jsonc before starting the bot again."; - mv ./temp_config/config.example.jsonc ./config/config.jsonc - exit 1; - fi -fi - -npx prisma db push --schema=./prisma/docker.prisma -npm run start \ No newline at end of file diff --git a/dockerfile b/dockerfile deleted file mode 100644 index 5b1d6503..00000000 --- a/dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM node:20.3-alpine - -# Setup workspace -WORKDIR /app -ENV DATABASE_URL=postgresql://postgres:postgres@pgsql:5432/postgres?schema=public - -# Copy runtime files -COPY ./config/ ./temp_config -COPY docker_run.sh . -COPY locales ./locales - -# Prisma builds -COPY prisma ./prisma -RUN npx prisma generate --schema=./prisma/docker.prisma - -# Install dependencies -COPY package.json . -RUN npm install - -# Transpile the source -COPY tsconfig.json . -COPY src ./src -RUN npm run build - -# Setup Bash -RUN apk add --no-cache bash -RUN chmod +x ./docker_run.sh - -# Start the server -ENTRYPOINT ["./docker_run.sh"] \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 00000000..5d43f6d4 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,13 @@ +import { config } from "dotenv"; +import { defineConfig } from "drizzle-kit"; + +config({ path: "./config/.env" }); + +export default defineConfig({ + out: "./drizzle", + schema: "./src/db/schema.ts", + dialect: "sqlite", + dbCredentials: { + url: process.env.DB_FILE_NAME + } +}); diff --git a/locales/cs.json b/locales/cs.json deleted file mode 100644 index 410852a1..00000000 --- a/locales/cs.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Otevřít ticket", - "description": "Kliknutím na tlačítko otevřete ticket", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Ticket CATEGORYNAME", - "description": "Náš tým vám brzy odpoví!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Ticket byl uzavřen", - "description": "Ticket uzavřel CLOSERNAME z důvodu: `REASON`" - }, - "ticketClosedDM": { - "title": "Ticket byl uzavřen", - "description": "Ticket n°TICKETCOUNT byl uzavřen. Uzavřel ho CLOSERNAME z důvodu: `REASON`\n\nZde máš přepis ticketu: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Otevřít ticket", - "label": "Zadej důvod k otevření ticketu", - "placeholder": "Prosím zadej důvod k otevření ticketu" - }, - "reasonTicketClose": { - "title": "Zavřít ticket", - "label": "Zadej důvod k zavření ticketu", - "placeholder": "Prosím zadej důvod k zavření ticketu" - } - }, - "buttons": { - "close": { - "label": "Zavřít ticket", - "emoji": "🔒" - }, - "claim": { - "label": "Vzít", - "emoji": "🙋" - } - }, - "invalidConfig": "Byla zjištěna neplatná konfigurace. Požádejte operátora bota, aby problém vyřešil!", - "ticketOpenedMessage": "Ticket byl otevřen! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Pouze admin půže vzít ticket", - "ticketAlreadyClaimed": "Ticket už je sebrán jiným adminem!", - "ticketClaimedMessage": "> Ticket vzal USER", - "ticketOnlyClosableByStaff": "Pouze admin může zavřít ticket!", - "ticketAlreadyClosed": "Ticket už je uzavřen!", - "ticketCreatingTranscript": "> Vytvářím přepis...", - "ticketTranscriptCreated": "> Přepis byl vytvořen! TRANSCRIPTURL", - "ticketLimitReached": "Můžeš mít pouze TICKETLIMIT otevřených ticketu!", - - "other": { - "openTicketButtonMSG": "Otevřít ticket", - "deleteTicketButtonMSG": "Smazat ticket", - "selectTicketTypePlaceholder": "Vyber typ ticketu", - "claimedBy": "**Tento ticket vzal**: USER", - "noReasonGiven": "Důvod nebyl udán!", - "unavailable": "Nedostupné" - } -} diff --git a/locales/de.json b/locales/de.json deleted file mode 100644 index 470e9e2b..00000000 --- a/locales/de.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Öffne ein Ticket", - "description": "Klicke auf den Button, um ein Ticket zu öffnen", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Ticket CATEGORYNAME", - "description": "Ein Teammitglied wird sich in Kürze um dich kümmern!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Ticket geschlossen", - "description": "Das Ticket wurde von CLOSERNAME mit folgendem Grund geschlossen: `REASON`" - }, - "ticketClosedDM": { - "title": "Ticket geschlossen", - "description": "Das Ticket n°TICKETCOUNT wurde von CLOSERNAME mit folgendem Grund geschlossen: `REASON`\n\nHier findest du das Transkript: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Ticket öffnen", - "label": "Grund des Tickets", - "placeholder": "Bitte gib einen Grund für das Öffnen des Tickets an." - }, - "reasonTicketClose": { - "title": "Ticket schließen", - "label": "Grund der Schließung", - "placeholder": "Bitte gib einen Grund für die Schließung des Tickets an." - } - }, - "buttons": { - "close": { - "label": "Ticket schließen", - "emoji": "🔒" - }, - "claim": { - "label": "Beanspruchen", - "emoji": "🙋" - } - }, - "invalidConfig": "Ungültige Konfiguration erkannt. Bitten Sie den Bot-Betreiber, das Problem zu beheben!", - "ticketOpenedMessage": "Ticket geöffnet! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Das Ticket kann nur von Teammitgliedern beansprucht werden!", - "ticketAlreadyClaimed": "Dieses Ticket wurde schon beansprucht!", - "ticketClaimedMessage": "> Ticket von USER beansprucht", - "ticketOnlyClosableByStaff": "Das Ticket kann nur von Teammitgliedern geschlossen werden!", - "ticketAlreadyClosed": "Dieses Ticket wurde schon geschlossen!", - "ticketCreatingTranscript": "> Generiere Transkript...", - "ticketTranscriptCreated": "> Transkript generiert! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Nur Teammitglieder können den Namen des tickets ändern!", - "ticketRenamed": "> Ticketname geändert zu NEWNAME", - "ticketLimitReached": "Du kannst nur TICKETLIMIT Tickets gleichzeitig offen haben", - "noTickets": "Sie haben keinen Zugriff auf Tickets", - - "other": { - "openTicketButtonMSG": "Öffne ein Ticket", - "deleteTicketButtonMSG": "Ticket löschen", - "selectTicketTypePlaceholder": "Wähle einen Typ", - "claimedBy": "**Beansprucht von**: USER", - "noReasonGiven": "Kein Grund angegeben", - "unavailable": "Unavailable" - } -} diff --git a/locales/el.json b/locales/el.json deleted file mode 100644 index 52054c3b..00000000 --- a/locales/el.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Ανοίξτε ένα ticket", - "description": "Κάντε κλικ στο κουμπί για να ξεκινήσετε το άνοιγμα ενός ticket", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Ticket CATEGORYNAME", - "description": "Ένα προσωπικό θα σας απαντήσει το συντομότερο δυνατό!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Ticket έκλεισε", - "description": "Το ticket έκλεισε από τον CLOSERNAME με τον εξής λόγο: `REASON`." - }, - "ticketClosedDM": { - "title": "Ticket έκλεισε", - "description": "Το ticket n°TICKETCOUNT έκλεισε από τον CLOSERNAME με τον εξής λόγο: `REASON`\n\nΑκολουθεί η μεταγραφή του ticket: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Ανοίξτε ένα ticket", - "label": "Ο λόγος του ticket σας", - "placeholder": "Εισαγάγετε τον λόγο για τον οποίο ανοίγετε ένα ticket" - }, - "reasonTicketClose": { - "title": "Κλείσιμο ticket", - "label": "Ο λόγος που έκλεισε το ticket", - "placeholder": "Εισαγάγετε τον λόγο για τον οποίο κλείνετε το ticket" - } - }, - "buttons": { - "close": { - "label": "Κλείσιμο ticket", - "emoji": "🔒" - }, - "claim": { - "label": "Διεκδίκηση", - "emoji": "🙋" - } - }, - "invalidConfig": "Εντοπίστηκε μη έγκυρη διαμόρφωση, ζητήστε από τον χειριστή του bot να τη διορθώσει!", - "ticketOpenedMessage": "Το ticket άνοιξε! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Το ticket μπορεί να διεκδικηθεί μόνο από προσωπικό!", - "ticketAlreadyClaimed": "Το ticket έχει ήδη διεκδικηθεί!", - "ticketClaimedMessage": "> Διεκδίκηση ticket από USER", - "ticketOnlyClosableByStaff": "Μόνο το προσωπικό μπορεί να κλείσει το ticket!", - "ticketAlreadyClosed": "Το ticket είναι ήδη κλειστό!", - "ticketCreatingTranscript": "> Δημιουργία μεταγραφής...", - "ticketTranscriptCreated": "> Δημιουργήθηκε η μεταγραφή! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Μόνο το προσωπικό μπορεί να μετονομάσει τα ticket!", - "ticketRenamed": "> Το ticket μετονομάστηκε σε NEWNAME", - "ticketLimitReached": "Μπορείτε να ανοίξετε μόνο TICKETLIMIT tickets ταυτόχρονα!", - "noTickets": "Δεν έχετε πρόσβαση σε κανένα ticket", - - "other": { - "openTicketButtonMSG": "Ανοίξτε ένα ticket", - "deleteTicketButtonMSG": "Διαγραφή ticket", - "selectTicketTypePlaceholder": "Επιλέξτε έναν τύπο ticket", - "claimedBy": "**Διεκδικημένο από**: USER", - "noReasonGiven": "Δεν δόθηκε λόγος", - "unavailable": "Μη διαθέσιμο" - } -} diff --git a/locales/es.json b/locales/es.json deleted file mode 100644 index 6f40697c..00000000 --- a/locales/es.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Abrir un ticket", - "description": "Presiona el botón de abajo para abrir un ticket", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Ticket CATEGORYNAME", - "description": "Un miembro del equipo te responderá dentro de un momento", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Ticket cerrado", - "description": "El ticket fue cerrado por CLOSERNAME con la razón: `REASON`" - }, - "ticketClosedDM": { - "title": "Ticket cerrado", - "description": "El ticket n°TICKETCOUNT se cerró por CLOSERNAME con la siguiente razón: `REASON`\n\nAquí tienes la traducción: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Abrir ticket", - "label": "Razón por que abres", - "placeholder": "Ingresa la razón" - }, - "reasonTicketClose": { - "title": "Cerrar ticket", - "label": "Razón del cierre", - "placeholder": "Ingresa la razón" - } - }, - "buttons": { - "close": { - "label": "Cerrar ticket", - "emoji": "🔒" - }, - "claim": { - "label": "Reclamar", - "emoji": "🙋" - } - }, - "invalidConfig": "Se detectó una configuración no válida, ¡pídale al operador del bot que la arregle!", - "ticketOpenedMessage": "¡Ticket abierto! --> TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "¡El ticket solo puede ser reclamado por un miembro del equipo!", - "ticketAlreadyClaimed": "¡Ya se reclamó el ticket!", - "ticketClaimedMessage": "> Ticket reclamado por USER", - "ticketOnlyClosableByStaff": "¡El ticket solo puede ser cerrado por un miembro del equipo!", - "ticketAlreadyClosed": "¡El ticket ya estaba cerrado!", - "ticketCreatingTranscript": "> Transcribiendo...", - "ticketTranscriptCreated": "> ¡Ticket transcripto! TRANSCRIPTURL", - "ticketLimitReached": "Solo puedes tener TICKETLIMIT tickets abiertos al mismo tiempo!", - - "other": { - "openTicketButtonMSG": "Abrir un ticket", - "deleteTicketButtonMSG": "Eliminar ticket", - "selectTicketTypePlaceholder": "Elegir el tipo de ticket", - "claimedBy": "**Reclamado por**: USER", - "noReasonGiven": ":x: No se dio una razón", - "unavailable": "Unavailable" - } -} diff --git a/locales/fr.json b/locales/fr.json deleted file mode 100644 index 3ae0312e..00000000 --- a/locales/fr.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Ouvrir un ticket", - "description": "Cliquez sur le bouton ci-dessous pour ouvrir un ticket.", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Ticket CATEGORYNAME", - "description": "Votre ticket a été ouvert. Un membre du staff va vous répondre dans les plus brefs délais.", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Ticket fermé", - "description": "Le ticket a été fermé par CLOSERNAME avec comme raison: `REASON`" - }, - "ticketClosedDM": { - "title": "Ticket fermé", - "description": "Le ticket n°TICKETCOUNT a été fermé par CLOSERNAME avec comme raison: `REASON`\n\nVoici une retranscription du ticket: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Ouvrir un ticket", - "label": "La raison de votre ticket", - "placeholder": "Veuillez entrer la raison de l'ouverture de votre ticket." - }, - "reasonTicketClose": { - "title": "Fermer un ticket", - "label": "La raison de la fermeture du ticket", - "placeholder": "Veuillez entrer la raison de la fermeture de votre ticket." - } - }, - "buttons": { - "close": { - "label": "Fermer le ticket", - "emoji": "🔒" - }, - "claim": { - "label": "Réclamer", - "emoji": "🙋" - } - }, - "invalidConfig": "Configuration non valide détectée, veuillez demander à l'opérateur du bot de la corriger!", - "ticketOpenedMessage": "Ticket ouvert! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Seuls les membres du staff peuvent réclamer un ticket !", - "ticketAlreadyClaimed": "Le ticket est déjà reçu!", - "ticketClaimedMessage": "> Ticket reçu par USER", - "ticketOnlyClosableByStaff": "Seuls les membres du staff peuvent fermer le ticket !", - "ticketAlreadyClosed": "Ticket déjà fermé !", - "ticketCreatingTranscript": "> Retranscription...", - "ticketTranscriptCreated": "> Retranscription créée ! TRANSCRIPTURL", - "ticketRenamed": "> Ticket renommé en NEWNAME", - "ticketLimitReached": "Vous ne pouvez seulement que TICKETLIMIT ticket(s) ouvert(s) !", - - "other": { - "openTicketButtonMSG": "Ouvrir un ticket", - "deleteTicketButtonMSG": "Supprimer un ticket", - "selectTicketTypePlaceholder": "Sélectionnez le type de ticket", - "claimedBy": "**Claim par**: USER", - "noReasonGiven": "Aucune raison donnée", - "unavailable": "Unavailable" - } -} diff --git a/locales/id.json b/locales/id.json deleted file mode 100644 index 901de4e5..00000000 --- a/locales/id.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Buka tiket", - "description": "Klik tombol untuk mulai membuka tiket", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Tiket CATEGORYNAME", - "description": "Staff akan membalas Anda sesegera mungkin!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Tiket ditutup", - "description": "Tiket telah ditutup oleh CLOSERNAME dengan alasan berikut: `REASON`", - "deleteTicketInfo": "> Tiket akan dihapus dalam 15 detik" - }, - "ticketClosedDM": { - "title": "Tiket ditutup", - "description": "Tiket nomor TICKETCOUNT telah ditutup oleh CLOSERNAME dengan alasan berikut: `REASON`\n\nBerikut transkrip tiket: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Buka tiket", - "label": "Alasan Anda membuka tiket", - "placeholder": "Silakan masukkan alasan mengapa Anda membuka tiket" - }, - "reasonTicketClose": { - "title": "Tutup tiket", - "label": "Alasan penutupan tiket", - "placeholder": "Silakan masukkan alasan mengapa Anda menutup tiket" - } - }, - "buttons": { - "close": { - "label": "Tutup tiket", - "emoji": "🔒" - }, - "claim": { - "label": "Klaim", - "emoji": "🙋" - } - }, - "invalidConfig": "Konfigurasi tidak valid terdeteksi, silakan minta operator bot untuk memperbaikinya!", - "ticketOpenedMessage": "Tiket dibuka! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Tiket hanya dapat diklaim oleh staff!", - "ticketAlreadyClaimed": "Tiket sudah diklaim!", - "ticketClaimedMessage": "> Tiket diklaim oleh USER", - "ticketOnlyClosableByStaff": "Hanya staff yang dapat menutup tiket!", - "ticketAlreadyClosed": "Tiket sudah ditutup!", - "ticketCreatingTranscript": "> Membuat transkrip...", - "ticketTranscriptCreated": "> Transkrip dibuat! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Hanya staff yang dapat mengubah nama tiket!", - "ticketRenamed": "> Tiket diubah namanya menjadi NEWNAME", - "ticketLimitReached": "Anda hanya dapat memiliki TICKETLIMIT tiket yang dibuka pada saat yang bersamaan!", - "noTickets": "Anda tidak memiliki akses ke tiket apa pun", - - "other": { - "openTicketButtonMSG": "Buka tiket", - "deleteTicketButtonMSG": "Hapus tiket", - "selectTicketTypePlaceholder": "Pilih jenis tiket", - "claimedBy": "**Diklaim Oleh**: USER", - "noReasonGiven": "Tidak ada alasan yang diberikan", - "unavailable": "Tidak tersedia" - } -} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json deleted file mode 100644 index 6098ed30..00000000 --- a/locales/it.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Apri un ticket", - "description": "Clicca sul pulsante per iniziare ad aprire un ticket", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Ticket CATEGORYNAME", - "description": "Un membro dello staff ti risponderà il prima possibile!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Ticket chiuso", - "description": "Il ticket è stato chiuso da CLOSERNAME con la seguente motivazione: `REASON`", - "deleteTicketInfo": "> Il ticket verrà eliminato in 15 secondi" - }, - "ticketClosedDM": { - "title": "Ticket chiuso", - "description": "Il ticket n°TICKETCOUNT è stato chiuso da CLOSERNAME con la seguente motivazione: `REASON`\n\nEcco la trascrizione del ticket: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Apri ticket", - "label": "La motivazione del tuo ticket", - "placeholder": "Per favore inserisci la motivazione per cui stai aprendo un ticket" - }, - "reasonTicketClose": { - "title": "Chiudi ticket", - "label": "La motivazione della chiusura del ticket", - "placeholder": "Per favore inserisci la motivazione per cui stai chiudendo il ticket" - } - }, - "buttons": { - "close": { - "label": "Chiudi ticket", - "emoji": "🔒" - }, - "claim": { - "label": "Richiedi", - "emoji": "🙋" - } - }, - "invalidConfig": "Configurazione non valida rilevata, per favore chiedi all'operatore del bot di sistemarla!", - "ticketOpenedMessage": "Ticket aperto! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Il ticket può essere richiesto solo dallo staff!", - "ticketAlreadyClaimed": "Il ticket è già stato richiesto!", - "ticketClaimedMessage": "> Ticket richiesto da USER", - "ticketOnlyClosableByStaff": "Solo lo staff può chiudere il ticket!", - "ticketAlreadyClosed": "Il ticket è già chiuso!", - "ticketCreatingTranscript": "> Creazione trascrizione...", - "ticketTranscriptCreated": "> Trascrizione creata! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Solo lo staff può rinominare i ticket!", - "ticketRenamed": "> Ticket rinominato in NEWNAME", - "ticketLimitReached": "Puoi avere solo TICKETLIMIT ticket aperti contemporaneamente!", - "noTickets": "Non hai accesso a nessun ticket", - "other": { - "openTicketButtonMSG": "Apri un ticket", - "deleteTicketButtonMSG": "Elimina ticket", - "selectTicketTypePlaceholder": "Seleziona un tipo di ticket", - "claimedBy": "**Richiesto Da**: USER", - "noReasonGiven": "Nessuna motivazione fornita", - "unavailable": "Non disponibile" - } -} \ No newline at end of file diff --git a/locales/main.json b/locales/main.json deleted file mode 100644 index ca7f6564..00000000 --- a/locales/main.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Open a ticket", - "description": "Click on the button to start opening a ticket", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Ticket CATEGORYNAME", - "description": "A staff will reply you as soon as possible!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Ticket closed", - "description": "The ticket has been closed by CLOSERNAME with the following reason: `REASON`", - "deleteTicketInfo": "> The ticket will be deleted in 15 seconds" - }, - "ticketClosedDM": { - "title": "Ticket closed", - "description": "The ticket n°TICKETCOUNT has been closed by CLOSERNAME with the following reason: `REASON`\n\nHere is the transcript of the ticket: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Open ticket", - "label": "The reason of your ticket", - "placeholder": "Please enter the reason of why you are opening a ticket" - }, - "reasonTicketClose": { - "title": "Close ticket", - "label": "The reason of the ticket closure", - "placeholder": "Please enter the reason of why you are closing the ticket" - } - }, - "buttons": { - "close": { - "label": "Close ticket", - "emoji": "🔒" - }, - "claim": { - "label": "Claim", - "emoji": "🙋" - } - }, - "invalidConfig": "Invalid configuration detected, please ask the bot operator to fix it!", - "ticketOpenedMessage": "Ticket opened! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "The ticket can only be claimed by a staff!", - "ticketAlreadyClaimed": "The ticket is already claimed!", - "ticketClaimedMessage": "> Ticket claimed by USER", - "ticketOnlyClosableByStaff": "Only staff can close the ticket!", - "ticketAlreadyClosed": "The ticket is already closed!", - "ticketCreatingTranscript": "> Creating transcript...", - "ticketTranscriptCreated": "> Transcript created! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Only staff can rename tickets!", - "ticketRenamed": "> Ticket renamed to NEWNAME", - "ticketLimitReached": "You can only have TICKETLIMIT tickets opened at the same time!", - "noTickets": "You don't have access to any tickets", - - "other": { - "openTicketButtonMSG": "Open a ticket", - "deleteTicketButtonMSG": "Delete ticket", - "selectTicketTypePlaceholder": "Select a ticket type", - "claimedBy": "**Claimed By**: USER", - "noReasonGiven": "No reason given", - "unavailable": "Unavailable" - } -} diff --git a/locales/my.json b/locales/my.json deleted file mode 100644 index 9f1a4f6c..00000000 --- a/locales/my.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Buka tiket", - "description": "Klik butang untuk mula membuka tiket", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Tiket CATEGORYNAME", - "description": "Staf akan membalas anda secepat mungkin!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Tiket ditutup", - "description": "Tiket telah ditutup oleh CLOSERNAME dengan alasan berikut: `REASON`", - "deleteTicketInfo": "> Tiket akan dipadam dalam 15 saat" - }, - "ticketClosedDM": { - "title": "Tiket ditutup", - "description": "Tiket bernombor TICKETCOUNT telah ditutup oleh CLOSERNAME dengan alasan berikut: `REASON`\n\nBerikut adalah transkrip tiket: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Buka tiket", - "label": "Alasan anda membuka tiket", - "placeholder": "Sila masukkan alasan mengapa anda membuka tiket" - }, - "reasonTicketClose": { - "title": "Tutup tiket", - "label": "Alasan penutupan tiket", - "placeholder": "Sila masukkan alasan mengapa anda menutup tiket" - } - }, - "buttons": { - "close": { - "label": "Tutup tiket", - "emoji": "🔒" - }, - "claim": { - "label": "Tuntut", - "emoji": "🙋" - } - }, - "invalidConfig": "Pengesanan konfigurasi tidak sah, sila minta pengendali bot untuk membetulkannya!", - "ticketOpenedMessage": "Tiket dibuka! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Tiket hanya boleh dituntut oleh staf!", - "ticketAlreadyClaimed": "Tiket telah dituntut!", - "ticketClaimedMessage": "> Tiket dituntut oleh USER", - "ticketOnlyClosableByStaff": "Hanya staf boleh menutup tiket!", - "ticketAlreadyClosed": "Tiket sudah ditutup!", - "ticketCreatingTranscript": "> Mencipta transkrip...", - "ticketTranscriptCreated": "> Transkrip dicipta! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Hanya staf boleh menukar nama tiket!", - "ticketRenamed": "> Tiket ditukar nama kepada NEWNAME", - "ticketLimitReached": "Anda hanya boleh membuka TICKETLIMIT tiket pada satu masa!", - "noTickets": "Anda tidak mempunyai akses kepada mana-mana tiket", - - "other": { - "openTicketButtonMSG": "Buka tiket", - "deleteTicketButtonMSG": "Padam tiket", - "selectTicketTypePlaceholder": "Pilih jenis tiket", - "claimedBy": "**Dituntut Oleh**: USER", - "noReasonGiven": "Tiada alasan diberikan", - "unavailable": "Tidak tersedia" - } -} diff --git a/locales/ru.json b/locales/ru.json deleted file mode 100644 index e1097ddc..00000000 --- a/locales/ru.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Открыть заявку", - "description": "Нажмите на кнопку, чтобы начать процесс открытия заявки", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Заявка в категории CATEGORYNAME", - "description": "Сотрудник ответит вам как можно скорее!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Заявка закрыта", - "description": "Заявка была закрыта CLOSERNAME с следующей причиной: `REASON`" - }, - "ticketClosedDM": { - "title": "Заявка закрыта", - "description": "Заявка №TICKETCOUNT была закрыта CLOSERNAME с следующей причиной: `REASON`\n\nВот транскрипт заявки: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Открыть заявку", - "label": "Причина вашей заявки", - "placeholder": "Пожалуйста, укажите причину открытия заявки" - }, - "reasonTicketClose": { - "title": "Закрыть заявку", - "label": "Причина закрытия заявки", - "placeholder": "Пожалуйста, укажите причину закрытия заявки" - } - }, - "buttons": { - "close": { - "label": "Закрыть заявку", - "emoji": "🔒" - }, - "claim": { - "label": "Заявка принята", - "emoji": "🙋" - } - }, - "invalidConfig": "Обнаружена недопустимая конфигурация, пожалуйста, обратитесь к оператору бота для исправления!", - "ticketOpenedMessage": "Заявка открыта! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Заявку может принять только сотрудник!", - "ticketAlreadyClaimed": "Заявка уже принята!", - "ticketClaimedMessage": "> Заявка принята пользователем USER", - "ticketOnlyClosableByStaff": "Заявку может закрыть только сотрудник!", - "ticketAlreadyClosed": "Заявка уже закрыта!", - "ticketCreatingTranscript": "> Создание транскрипта...", - "ticketTranscriptCreated": "> Транскрипт создан! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Заявку может переименовать только сотрудник!", - "ticketRenamed": "> Заявка переименована в NEWNAME", - "ticketLimitReached": "Вы можете иметь только TICKETLIMIT открытых заявок одновременно!", - "noTickets": "У вас нет доступа к заявкам", - - "other": { - "openTicketButtonMSG": "Открыть заявку", - "deleteTicketButtonMSG": "Удалить заявку", - "selectTicketTypePlaceholder": "Выберите тип заявки", - "claimedBy": "**Принята пользователем**: USER", - "noReasonGiven": "Причина не указана", - "unavailable": "Недоступно" - } -} diff --git a/locales/tr.json b/locales/tr.json deleted file mode 100644 index c6531887..00000000 --- a/locales/tr.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Destek Talebi Başlatın", - "description": "Destek talebi başlatmak için butona basın", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Destek Talebi CATEGORYNAME", - "description": "Bir yetkili sizinle en hızlı şekilde iletişime geçecek!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Destek talebi kapatıldı", - "description": "Destek talebi, CLOSERNAME tarafından şu sebeple kapatıldı: `REASON`" - }, - "ticketClosedDM": { - "title": "Destek talebiniz kapatıldı", - "description": "TICKETCOUNT nolu destek talebiniz, CLOSERNAME tarafından şu sebeple kapatıldı: `REASON`\n\nDestek talebinizin bir transkripti: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Destek Talebi Başlatın", - "label": "Destek talebinizin sebebi", - "placeholder": "Lütfen neden destek talebi açtığınızı anlatın" - }, - "reasonTicketClose": { - "title": "Destek Talebi Kapatın", - "label": "Destek talebini kapatmanızın sebebi", - "placeholder": "Lütfen neden destek talebi kapattığınızı anlatın." - } - }, - "buttons": { - "close": { - "label": "Destek talebini kapat", - "emoji": "🔒" - }, - "claim": { - "label": "Destek talebini al", - "emoji": "🙋" - } - }, - "invalidConfig": "Geçersiz yapılandırma algılandı, lütfen bot operatöründen düzeltmesini isteyin!", - "ticketOpenedMessage": "Bir destek talebi başlatıldı! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Destek talepleri sadece yetkililer tarafından alınabilir!", - "ticketAlreadyClaimed": "Bu destek talebi hali hazırda başka bir yetkili tarafından alındı!", - "ticketClaimedMessage": "> Destek talebi USER tarafından alındı", - "ticketOnlyClosableByStaff": "Destek talepleri sadece yetkililer tarafından kapatılabilir!", - "ticketAlreadyClosed": "Destek talebi zaten kapalı!", - "ticketCreatingTranscript": "> Transkript oluşturuluyor...", - "ticketTranscriptCreated": "> Transkript oluşturuldu! TRANSCRIPTURL", - "ticketLimitReached": "Aynı anda en fazla TICKETLIMIT destek talebi açabilirsiniz!", - - "other": { - "openTicketButtonMSG": "Destek talebi aç", - "deleteTicketButtonMSG": "Destek talebini sil", - "selectTicketTypePlaceholder": "Destek talebi türü seçin", - "claimedBy": "USER **tarafından alındı**", - "noReasonGiven": "Sebep belirtilmedi.", - "unavailable": "Unavailable" - } -} diff --git a/locales/uk.json b/locales/uk.json deleted file mode 100644 index 87fdde68..00000000 --- a/locales/uk.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Відкрити тікет", - "description": "Натисніть на кнопку щоб відкрити тікет", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Тікет CATEGORYNAME", - "description": "Персонал надасть відповідь якомога швидше!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Тікет закрито", - "description": "Тікет був закритий користувачем CLOSERNAME з наступної причини: `REASON`" - }, - "ticketClosedDM": { - "title": "Тікет закрито", - "description": "Тікет n°TICKETCOUNT був закритий користувачем CLOSERNAME з наступної причини: `REASON`\n\nОсь транскрипція історії листування: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Відкрити тікет", - "label": "Причина вашого тікету", - "placeholder": "Будь ласка, введіть причину вашого тікету" - }, - "reasonTicketClose": { - "title": "Закрити тікет", - "label": "Причина закриття тікету", - "placeholder": "Будь ласка, введіть причину, чому ви закриваєте тікет" - } - }, - "buttons": { - "close": { - "label": "Закрити тікет", - "emoji": "🔒" - }, - "claim": { - "label": "Присвоїти собі", - "emoji": "🙋" - } - }, - "invalidConfig": "Недійсна конфігурація, будь ласка, повідомте про це відповідального за бота!", - "ticketOpenedMessage": "Тікет закрито! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Тікети можуть присвоювати собі тільки модератори!", - "ticketAlreadyClaimed": "Цей тікет вже був присвоєний!", - "ticketClaimedMessage": "> Тікет був присвоєний модераторові USER", - "ticketOnlyClosableByStaff": "Тільки персонал може закривати тікети!", - "ticketAlreadyClosed": "Тікет вже закритий!", - "ticketCreatingTranscript": "> Створення транскрипції...", - "ticketTranscriptCreated": "> Транскрипцію створено! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Тільки персонал може змінювати назву тікетів!", - "ticketRenamed": "> Назву тікету змінено на: NEWNAME", - "ticketLimitReached": "Ви можете мати тільки TICKETLIMIT тікетів відкритих водночас!", - "noTickets": "Ви не маєте доступу до тікетів", - - "other": { - "openTicketButtonMSG": "Відкрити тікет", - "deleteTicketButtonMSG": "Видалити тікет", - "selectTicketTypePlaceholder": "Вибрати тип тікету", - "claimedBy": "**Присвоєно**: USER", - "noReasonGiven": "Причину не вказано", - "unavailable": "Недоступно" - } -} diff --git a/locales/vi.json b/locales/vi.json deleted file mode 100644 index 8266cf06..00000000 --- a/locales/vi.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "embeds": { - "openTicket": { - "title": "Mở một phiếu hỗ trợ", - "description": "Nhấn vào nút để bắt đầu mở một phiếu hỗ trợ", - "footer": { - "text": "ticket.pm" - } - }, - "ticketOpened": { - "title": "Phiếu hỗ trợ CATEGORYNAME", - "description": "Một nhân viên sẽ phản hồi bạn ngay lập tức!", - "footer": { - "text": "ticket.pm" - } - }, - "ticketClosed": { - "title": "Phiếu hỗ trợ đã đóng", - "description": "Phiếu hỗ trợ đã được CLOSERNAME đóng với lý do sau: `REASON`" - }, - "ticketClosedDM": { - "title": "Phiếu hỗ trợ đã đóng", - "description": "Phiếu hỗ trợ số n°TICKETCOUNT đã được CLOSERNAME đóng với lý do sau: `REASON`\n\nDưới đây là bản ghi chép của phiếu hỗ trợ: TRANSCRIPTURL", - "footer": { - "text": "ticket.pm" - } - } - }, - "modals": { - "reasonTicketOpen": { - "title": "Mở phiếu hỗ trợ", - "label": "Lý do của phiếu hỗ trợ của bạn", - "placeholder": "Vui lòng nhập lý do bạn đang mở một phiếu hỗ trợ" - }, - "reasonTicketClose": { - "title": "Đóng phiếu hỗ trợ", - "label": "Lý do đóng phiếu hỗ trợ", - "placeholder": "Vui lòng nhập lý do bạn đang đóng phiếu hỗ trợ" - } - }, - "buttons": { - "close": { - "label": "Đóng phiếu hỗ trợ", - "emoji": "🔒" - }, - "claim": { - "label": "Đăng ký", - "emoji": "🙋" - } - }, - "invalidConfig": "Phát hiện cấu hình không hợp lệ, vui lòng yêu cầu người quản trị bot sửa chữa!", - "ticketOpenedMessage": "Phiếu hỗ trợ đã mở! TICKETCHANNEL", - "ticketOnlyClaimableByStaff": "Chỉ nhân viên có thể đăng ký phiếu hỗ trợ này!", - "ticketAlreadyClaimed": "Phiếu hỗ trợ này đã được đăng ký!", - "ticketClaimedMessage": "> Phiếu hỗ trợ đã được đăng ký bởi USER", - "ticketOnlyClosableByStaff": "Chỉ nhân viên có thể đóng phiếu hỗ trợ!", - "ticketAlreadyClosed": "Phiếu hỗ trợ này đã được đóng!", - "ticketCreatingTranscript": "> Đang tạo bản ghi chép...", - "ticketTranscriptCreated": "> Bản ghi chép đã được tạo! TRANSCRIPTURL", - "ticketOnlyRenamableByStaff": "Chỉ nhân viên có thể đổi tên phiếu hỗ trợ!", - "ticketRenamed": "> Phiếu hỗ trợ đã được đổi tên thành NEWNAME", - "ticketLimitReached": "Bạn chỉ có thể mở tối đa TICKETLIMIT phiếu hỗ trợ cùng một lúc!", - "noTickets": "Bạn không có quyền truy cập vào bất kỳ phiếu hỗ trợ nào", - - "other": { - "openTicketButtonMSG": "Mở một phiếu hỗ trợ", - "deleteTicketButtonMSG": "Xóa phiếu hỗ trợ", - "selectTicketTypePlaceholder": "Chọn loại phiếu hỗ trợ", - "claimedBy": "**Được đăng ký bởi**: USER", - "noReasonGiven": "Không có lý do nào được cung cấp", - "unavailable": "Không khả dụng" - } -} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fe8f5237..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6326 +0,0 @@ -{ - "name": "ticket-bot", - "version": "3.3.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ticket-bot", - "version": "3.3.1", - "license": "Apache-2.0", - "dependencies": { - "@prisma/client": "^5.4.2", - "axios": "^1.5.1", - "better-sqlite3": "^9.0.0", - "discord.js": "^14.13.0", - "dotenv": "^16.3.1", - "fs-extra": "^11.1.1", - "jsonc": "^2.0.0", - "mongoose": "^8.0.3", - "readline": "^1.3.0", - "ticket-bot-transcript-uploader": "^1.4.2-hotfix", - "websocket": "^1.0.34" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.5", - "@types/fs-extra": "^11.0.3", - "@types/node": "^22.7.5", - "@types/pg": "^8.10.7", - "@types/websocket": "^1.0.8", - "@typescript-eslint/eslint-plugin": "^8.8.1", - "@typescript-eslint/parser": "^8.8.1", - "eslint": "^8.51.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^5.0.0", - "prettier": "^3.0.3", - "prettier-eslint": "^16.3.0", - "prisma": "^5.4.1", - "rimraf": "^6.0.1", - "typescript": "^5.4.5" - } - }, - "node_modules/@discordjs/builders": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.2.tgz", - "integrity": "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/formatters": "^0.6.1", - "@discordjs/util": "^1.1.1", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.1", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", - "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.38.1" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.5.0.tgz", - "integrity": "sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.38.1", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.21.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.2.tgz", - "integrity": "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.5.0", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@types/ws": "^8.5.10", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "^0.38.1", - "tslib": "^2.6.2", - "ws": "^8.17.0" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz", - "integrity": "sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", - "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@prisma/client": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.13" - }, - "peerDependencies": { - "prisma": "*" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - } - } - }, - "node_modules/@prisma/debug": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/fetch-engine": "5.22.0", - "@prisma/get-platform": "5.22.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", - "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/get-platform": "5.22.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", - "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0" - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", - "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v16" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", - "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/jsonfile": "*", - "@types/node": "*" - } - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsonfile": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", - "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.15.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", - "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/pg": { - "version": "8.11.14", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.14.tgz", - "integrity": "sha512-qyD11E5R3u0eJmd1lB0WnWKXJGA7s015nyARWljfz5DcX83TKAIlY+QrmvzQTsbIe+hkiFtkyL2gHC6qwF6Fbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^4.0.1" - } - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/websocket": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.10.tgz", - "integrity": "sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.32.0", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", - "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz", - "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/bson": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", - "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bufferutil": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/discord-api-types": { - "version": "0.38.3", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.3.tgz", - "integrity": "sha512-vijevLh06Gtmex6BQzc9jRrGce6La0qnsF4bKwKM2L1ou0/sbJIOAkg7wz6YLLaodnUwQLljIhtrGxnkMjc1Ew==", - "license": "MIT" - }, - "node_modules/discord.js": { - "version": "14.19.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.19.3.tgz", - "integrity": "sha512-lncTRk0k+8Q5D3nThnODBR8fR8x2fM798o8Vsr40Krx0DjPwpZCuxxTcFMrXMQVOqM1QB9wqWgaXPg3TbmlHqA==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.11.2", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.1", - "@discordjs/rest": "^2.5.0", - "@discordjs/util": "^1.1.1", - "@discordjs/ws": "^1.2.2", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.38.1", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.21.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "license": "ISC", - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-airbnb-base": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", - "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", - "dev": true, - "license": "MIT", - "dependencies": { - "confusing-browser-globals": "^1.0.10", - "object.assign": "^4.1.2", - "object.entries": "^1.1.5", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "peerDependencies": { - "eslint": "^7.32.0 || ^8.2.0", - "eslint-plugin-import": "^2.25.2" - } - }, - "node_modules/eslint-config-airbnb-base/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" - } - }, - "node_modules/eslint-plugin-node/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-node/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", - "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "license": "MIT", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "license": "ISC", - "dependencies": { - "type": "^2.7.2" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", - "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "license": "MIT" - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jsonc/-/jsonc-2.0.0.tgz", - "integrity": "sha512-B281bLCT2TRMQa+AQUQY5AGcqSOXBOKaYGP4wDzoA/+QswUfN8sODektbPEs9Baq7LGKun5jQbNFpzwGuVYKhw==", - "license": "MIT", - "dependencies": { - "fast-safe-stringify": "^2.0.6", - "graceful-fs": "^4.1.15", - "mkdirp": "^0.5.1", - "parse-json": "^4.0.0", - "strip-bom": "^4.0.0", - "strip-json-comments": "^3.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "license": "MIT" - }, - "node_modules/loglevel": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", - "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, - "node_modules/loglevel-colored-level-prefix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", - "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.3", - "loglevel": "^1.4.1" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/magic-bytes.js": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", - "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/mongodb": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.16.0.tgz", - "integrity": "sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.3", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.14.1.tgz", - "integrity": "sha512-ijd12vjqUBr5Btqqflu0c/o8Oed5JpdaE0AKO9TjGxCgywYwnzt6ynR1ySjhgxGxrYVeXC0t1P11f1zlRiE93Q==", - "license": "MIT", - "dependencies": { - "bson": "^6.10.3", - "kareem": "2.6.3", - "mongodb": "~6.16.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "license": "MIT", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "license": "ISC" - }, - "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-numeric": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", - "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/pg-protocol": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.9.5.tgz", - "integrity": "sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==", - "dev": true, - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", - "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", - "dev": true, - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "pg-numeric": "1.0.2", - "postgres-array": "~3.0.1", - "postgres-bytea": "~3.0.0", - "postgres-date": "~2.1.0", - "postgres-interval": "^3.0.0", - "postgres-range": "^1.1.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postgres-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", - "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/postgres-bytea": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", - "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "obuf": "~1.1.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postgres-date": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", - "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/postgres-interval": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", - "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/postgres-range": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", - "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-eslint": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.4.1.tgz", - "integrity": "sha512-qf8Grhq68kg0tyhXVik2aOx72W8Jxre2ABXgV1pC3OCb3jOo40UZuvk3f/I3/ZA7Qw6qydZX4b7ySSCSgv4/8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/parser": "^6.21.0", - "common-tags": "^1.8.2", - "dlv": "^1.1.3", - "eslint": "^8.57.1", - "indent-string": "^4.0.0", - "lodash.merge": "^4.6.2", - "loglevel-colored-level-prefix": "^1.0.0", - "prettier": "^3.5.3", - "pretty-format": "^29.7.0", - "require-relative": "^0.8.7", - "vue-eslint-parser": "^9.4.3" - }, - "engines": { - "node": ">=16.10.0" - }, - "funding": { - "url": "https://opencollective.com/prettier-eslint" - }, - "peerDependencies": { - "prettier-plugin-svelte": "^3.0.0", - "svelte-eslint-parser": "*" - }, - "peerDependenciesMeta": { - "prettier-plugin-svelte": { - "optional": true - }, - "svelte-eslint-parser": { - "optional": true - } - } - }, - "node_modules/prettier-eslint/node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/prettier-eslint/node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/prettier-eslint/node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/prettier-eslint/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/prettier-eslint/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/prettier-eslint/node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/engines": "5.22.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=16.13" - }, - "optionalDependencies": { - "fsevents": "2.3.3" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readline": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", - "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", - "license": "BSD" - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-relative": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", - "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", - "license": "MIT" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/synckit": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", - "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ticket-bot-transcript-uploader": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ticket-bot-transcript-uploader/-/ticket-bot-transcript-uploader-1.4.3.tgz", - "integrity": "sha512-q64e3ImtVryVEXUrPODIf0aQ8NRQVrEY2MxzT931X/q2/piUEM1BHPBNT8+VMwy4yOyfSNRuAdv54gmT89LG5A==", - "license": "Apache-2.0", - "dependencies": { - "axios": "^1.6.8", - "discord.js": "^14.14.1" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-mixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", - "license": "MIT" - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "license": "ISC" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici": { - "version": "6.21.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", - "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/vue-eslint-parser": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", - "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", - "esquery": "^1.4.0", - "lodash": "^4.17.21", - "semver": "^7.3.6" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/websocket": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", - "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", - "license": "Apache-2.0", - "dependencies": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.63", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/websocket/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/websocket/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "engines": { - "node": ">=0.10.32" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index 32bedfeb..0bc23a63 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,25 @@ { - "name": "ticket-bot", - "version": "3.3.1", - "description": "Bot with a ticket system using Discord.js v14", - "main": "dist/index.js", - "scripts": { - "setup": "npm install && prisma db push", - "build": "rimraf dist && tsc", - "start": "node dist/index.js", - "format:fix": "prettier --write .", - "format:check": "prettier --check .", - "lint:fix": "eslint --fix . --ext .ts", - "lint:check": "eslint . --ext .ts" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/Sayrix/ticket-bot.git" - }, - "keywords": [ - "discord", - "ticket", - "bot" - ], - "author": "Sayrix", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/Sayrix/ticket-bot/issues" - }, - "homepage": "https://github.com/Sayrix/ticket-bot#readme", - "dependencies": { - "@prisma/client": "^5.4.2", - "axios": "^1.5.1", - "better-sqlite3": "^9.0.0", - "discord.js": "^14.13.0", - "dotenv": "^16.3.1", - "fs-extra": "^11.1.1", - "jsonc": "^2.0.0", - "mongoose": "^8.0.3", - "readline": "^1.3.0", - "ticket-bot-transcript-uploader": "^1.4.2-hotfix", - "websocket": "^1.0.34" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.6.5", - "@types/fs-extra": "^11.0.3", - "@types/node": "^22.7.5", - "@types/pg": "^8.10.7", - "@types/websocket": "^1.0.8", - "@typescript-eslint/eslint-plugin": "^8.8.1", - "@typescript-eslint/parser": "^8.8.1", - "eslint": "^8.51.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^5.0.0", - "prettier": "^3.0.3", - "prettier-eslint": "^16.3.0", - "prisma": "^5.4.1", - "rimraf": "^6.0.1", - "typescript": "^5.4.5" - } + "name": "ticket-bot", + "version": "4.0.0", + "description": "Open-source Discord ticket bot.", + "main": "src/index.ts", + "scripts": { + "start": "bun src/index.ts", + "format": "biome format", + "format:fix": "biome format --write", + "lint": "biome lint", + "i18n": "bunx typesafe-i18n --no-watch" + }, + "dependencies": { + "@libsql/client": "^0.17.2", + "dotenv": "^17.4.1", + "drizzle-orm": "^0.45.2", + "typesafe-i18n": "^5.27.1" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.10", + "@types/bun": "^1.3.11", + "@typescript/native-preview": "^7.0.0-dev.20260409.1", + "drizzle-kit": "^0.31.10" + } } diff --git a/prisma/compatible.sql b/prisma/compatible.sql deleted file mode 100644 index 44edd33d..00000000 --- a/prisma/compatible.sql +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright © 2023 小兽兽/zhiyan114 (github.com/zhiyan114) -File is licensed respectively under the terms of the Apache License 2.0 -or whichever license the project is using at the time https://github.com/Sayrix/Ticket-Bot/blob/main/LICENSE - -This file is from postgre.sql but modified for sqlite and mysql compatibility with prisma. -RANT: I LOVE AND HATE SQLITE. Why can't it just support a little more features... - -For production use, please use "prisma db push" instead or follow the documentation: https://doc.ticket.pm/docs/intro. -*/ - - -/* -this will be used for -* openTicketMessageId -* ticketCount -*/ - -CREATE TABLE IF NOT EXISTS config ( - key TEXT PRIMARY KEY, - value TEXT -); - -/* -this will be used for storing tickets -*/ - -CREATE TABLE IF NOT EXISTS tickets ( - id SERIAL PRIMARY KEY, - channelid TEXT NOT NULL UNIQUE, - messageid TEXT NOT NULL UNIQUE, - category LONGTEXT NOT NULL, - invited TEXT NOT NULL DEFAULT '[]', - reason TEXT NOT NULL, - creator TEXT NOT NULL, - createdat BIGINT NOT NULL, - claimedby TEXT, - claimedat BIGINT, - closedby TEXT, - closedat BIGINT, - closereason TEXT, - transcript TEXT -); diff --git a/prisma/docker.prisma b/prisma/docker.prisma deleted file mode 100644 index 3d784ed9..00000000 --- a/prisma/docker.prisma +++ /dev/null @@ -1,31 +0,0 @@ -// Docker uses postgresql as the main database -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model config { - key String @id - value String? @db.Text -} - -model tickets { - id Int @id @default(autoincrement()) - channelid String @unique - messageid String @unique - category String @db.Text - invited String @default("[]") @db.Text - reason String @db.Text - creator String @db.Text - createdat BigInt - claimedby String? @db.Text - claimedat BigInt? - closedby String? @db.Text - closedat BigInt? - closereason String? @db.Text - transcript String? @db.Text -} diff --git a/prisma/postgre.sql b/prisma/postgre.sql deleted file mode 100644 index 2b011b08..00000000 --- a/prisma/postgre.sql +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright © 2023 小兽兽/zhiyan114 (github.com/zhiyan114) -File is licensed respectively under the terms of the Apache License 2.0 -or whichever license the project is using at the time https://github.com/Sayrix/Ticket-Bot/blob/main/LICENSE - -For production use, please don't try to use this file, even if you're using postgresql, -Since the code is tailored towards compatibility.sql, it will break. -You have been warned. - -I wrote this in-case multi-db support will eventually be dropped, and I'm a big postgresql fan ^w^ -*/ - - - -/* -this will be used for -* openTicketMessageId -* ticketCount -*/ - -CREATE TABLE IF NOT EXISTS config ( - key VARCHAR(256) PRIMARY KEY, - value LONGTEXT -); - -/* -this will be used for storing tickets -*/ - -CREATE TABLE IF NOT EXISTS tickets ( - id SERIAL PRIMARY KEY, - channelid TEXT NOT NULL UNIQUE, - messageid TEXT NOT NULL UNIQUE, - category JSON NOT NULL, - reason TEXT NOT NULL, - creator TEXT NOT NULL, - createdat TIMESTAMP NOT NULL DEFAULT NOW(), - claimedby TEXT, - claimedat TIMESTAMP, - closedby TEXT, - closedat TIMESTAMP, - closereason TEXT, - transcript TEXT -); - -/* -this will be used to handle ticket invites -*/ - -CREATE TABLE IF NOT EXISTS invites ( - id SERIAL PRIMARY KEY, - ticketid TEXT NOT NULL, - userid TEXT NOT NULL, - CONSTRAINT FK_ticketID FOREIGN KEY(ticketid) REFERENCES tickets(messageid) ON DELETE CASCADE -); diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index a15fffbe..00000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,30 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = env("DATABASE_URL") -} - -model config { - key String @id - value String? -} - -model tickets { - id Int @id @default(autoincrement()) - channelid String @unique - messageid String @unique - category String - invited String @default("[]") - reason String - creator String - createdat BigInt - claimedby String? - claimedat BigInt? - closedby String? - closedat BigInt? - closereason String? - transcript String? -} diff --git a/src/commands/add.ts b/src/commands/add.ts deleted file mode 100644 index 13100f0f..00000000 --- a/src/commands/add.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {BaseCommand, ExtendedClient} from "../structure"; -import {CommandInteraction, SlashCommandBuilder, TextChannel, User} from "discord.js"; -import {log} from "../utils/logs"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -export default class AddCommand extends BaseCommand { - public static data: SlashCommandBuilder = new SlashCommandBuilder() - .setName("add") - .setDescription("Add someone to the ticket") - .addUserOption((input) => input.setName("user").setDescription("The user to add").setRequired(true)); - constructor(client: ExtendedClient) { - super(client); - } - - async execute(interaction: CommandInteraction) { - - const added = interaction.options.get("user", true).user as User; - const ticket = await this.client.prisma.tickets.findUnique({ - select: { - id: true, - invited: true, - }, - where: { - channelid: interaction.channel?.id - } - }); - - if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); - - const invited = JSON.parse(ticket.invited) as string[]; - if (invited.includes(added.id)) return interaction.reply({ content: "User already added", ephemeral: true }).catch((e) => console.log(e)); - - if (invited.length >= 25) - return interaction.reply({ content: "You can't add more than 25 users", ephemeral: true }).catch((e) => console.log(e)); - - invited.push(added.id); - await this.client.prisma.tickets.update({ - data: { - invited: JSON.stringify(invited) - }, - where: { - channelid: interaction.channel?.id - } - }); - - await (interaction.channel as TextChannel | null)?.permissionOverwrites - .edit(added, { - SendMessages: true, - AddReactions: true, - ReadMessageHistory: true, - AttachFiles: true, - ViewChannel: true, - }); - - await interaction.reply({ content: `> Added <@${added.id}> to the ticket` }).catch((e) => console.log(e)); - - log( - { - LogType: "userAdded", - user: interaction.user, - ticketId: ticket.id.toString(), - ticketChannelId: interaction.channel?.id, - target: added, - }, - this.client - ); - } -} - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/commands/claim.ts b/src/commands/claim.ts deleted file mode 100644 index 5e655067..00000000 --- a/src/commands/claim.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {BaseCommand, ExtendedClient} from "../structure"; -import {claim} from "../utils/claim"; -import {CommandInteraction, SlashCommandBuilder} from "discord.js"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -export default class ClaimCommand extends BaseCommand { - public static data: SlashCommandBuilder = new SlashCommandBuilder() - .setName("claim").setDescription("Set the ticket as claimed."); - constructor(client: ExtendedClient) { - super(client); - } - - async execute(interaction: CommandInteraction) { - return claim(interaction, this.client); - } -} - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ \ No newline at end of file diff --git a/src/commands/clearDM.ts b/src/commands/clearDM.ts deleted file mode 100644 index 3e89226a..00000000 --- a/src/commands/clearDM.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {BaseCommand, ExtendedClient} from "../structure"; -import {CommandInteraction, SlashCommandBuilder} from "discord.js"; - -/* -Copyright © 2024 小兽兽/zhiyan114 (github.com/zhiyan114) -File is licensed respectively under the terms of the Creative Commons Attribution 4.0 International -or whichever license the project is using at the time https://github.com/Sayrix/Ticket-Bot/blob/main/LICENSE.md -*/ - -export default class AddCommand extends BaseCommand { - public static data: SlashCommandBuilder = new SlashCommandBuilder() - .setName("cleardm") - .setDescription("Clear all of your ticket history in your DM"); - constructor(client: ExtendedClient) { - super(client); - } - - async execute(interaction: CommandInteraction) { - interaction.deferReply({ ephemeral: true }); - - const dm = await interaction.user.createDM(); - - let messages = (await dm.messages.fetch({ limit: 100 })) - .filter((message) => message.author.id === this.client.user?.id); - while(messages.size > 0) { - for(const message of messages) - await message[1].delete(); - if(messages.size < 100) - break; - messages = await dm.messages.fetch({ limit: 100 }); - } - await interaction.followUp({ content: "Cleared all of your DM history", ephemeral: true }); - } -} diff --git a/src/commands/close.ts b/src/commands/close.ts deleted file mode 100644 index 1fb31e53..00000000 --- a/src/commands/close.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { CommandInteraction, GuildMember, SlashCommandBuilder } from "discord.js"; -import { closeAskReason } from "../utils/close_askReason"; -import {close} from "../utils/close.js"; -import {BaseCommand, ExtendedClient, TicketType} from "../structure"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -export default class CloseCommand extends BaseCommand { - public static data: SlashCommandBuilder = new SlashCommandBuilder() - .setName("close").setDescription("Close the ticket"); - constructor(client: ExtendedClient) { - super(client); - } - - async execute(interaction: CommandInteraction) { - - // @TODO: Breaking change refactor happens here as well.. - - const ticket = await this.client.prisma.tickets.findUnique({ - where: { - channelid: interaction.channel?.id - } - }); - const ticketType = ticket ? JSON.parse(ticket.category) as TicketType : undefined; - - if ( - this.client.config.closeOption.whoCanCloseTicket === "STAFFONLY" && - !(interaction.member as GuildMember | null)?.roles.cache.some((r) => this.client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || - ticketType?.staffRoles?.includes(r.id)) - ) - return interaction - .reply({ - content: this.client.locales.getValue("ticketOnlyClosableByStaff"), - ephemeral: true, - }) - .catch((e) => console.log(e)); - - if (this.client.config.closeOption.askReason) { - closeAskReason(interaction, this.client); - } else { - await interaction.deferReply().catch((e) => console.log(e)); - close(interaction, this.client); - } } -} - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/commands/index.ts b/src/commands/index.ts deleted file mode 100644 index a750f174..00000000 --- a/src/commands/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import AddCommand from "./add"; -import MassAddCommand from "./massadd"; -import ClaimCommand from "./claim"; -import CloseCommand from "./close"; -import RemoveCommand from "./remove"; -import RenameCommand from "./rename"; -import clearDM from "./clearDM"; - -export { - AddCommand, - MassAddCommand, - ClaimCommand, - CloseCommand, - RemoveCommand, - RenameCommand, - clearDM -}; \ No newline at end of file diff --git a/src/commands/massadd.ts b/src/commands/massadd.ts deleted file mode 100644 index 13584347..00000000 --- a/src/commands/massadd.ts +++ /dev/null @@ -1,89 +0,0 @@ -import {BaseCommand, ExtendedClient} from "../structure"; -import {CommandInteraction, SlashCommandBuilder, TextChannel} from "discord.js"; -import {log} from "../utils/logs"; - -/* -Copyright © 2023 小兽兽/zhiyan114 (github.com/zhiyan114) -File is licensed respectively under the terms of the Apache License 2.0 -or whichever license the project is using at the time https://github.com/Sayrix/Ticket-Bot/blob/main/LICENSE -*/ - - -// Use add command if possible, otherwise you're missing out on proper user validation... -export default class MassAddCommand extends BaseCommand { - public static data: SlashCommandBuilder = new SlashCommandBuilder() - .setName("massadd") - .setDescription("Add multiple users to the ticket. It's recommended to use the regular add command when possible.") - .addStringOption((input) => input.setName("users").setDescription("Users to add. Use ',' as seperator.").setRequired(true)); - constructor(client: ExtendedClient) { - super(client); - } - - async execute(interaction: CommandInteraction) { - - // In-case users will try things - const users = await Promise.all((interaction.options.get("users", true).value as string) - .replace(/\s/g, "") // Remove space incase user adds it as a seperator - .split(",") // Get a list from it - .filter((user) => user !== "") // anti seperator spams at the end lmao - .map(async user => await this.client.users.fetch(user))); // Convert it to discord users objects - - // Additional checks - if(users.length == 0) return await interaction.reply({ content: "You need to specify at least one user", ephemeral: true }); - if(users.length > 25) return await interaction.reply({ content: "You can't add more than 25 users", ephemeral: true }); - - const ticket = await this.client.prisma.tickets.findUnique({ - select: { - id: true, - invited: true, - }, - where: { - channelid: interaction.channel?.id - } - }); - - if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); - - const invited = JSON.parse(ticket.invited) as string[]; - - for(const user of users) { - if (invited.includes(user.id)) - continue; - - if (invited.length >= 25) - break; - - invited.push(user.id); - - await (interaction.channel as TextChannel | null)?.permissionOverwrites - .edit(user, { - SendMessages: true, - AddReactions: true, - ReadMessageHistory: true, - AttachFiles: true, - ViewChannel: true, - }); - log( - { - LogType: "userAdded", - user: interaction.user, - ticketId: ticket.id.toString(), - ticketChannelId: interaction.channel?.id, - target: user, - }, - this.client - ); - } - - await this.client.prisma.tickets.update({ - data: { - invited: JSON.stringify(invited) - }, - where: { - channelid: interaction.channel?.id - } - }); - - await interaction.reply({ content: "> Mass User Add Completed! Do note that not all users may be added if internal checks failed. It's advise you use the regular add command to guarantee the add status." }); - } -} diff --git a/src/commands/remove.ts b/src/commands/remove.ts deleted file mode 100644 index 875621ea..00000000 --- a/src/commands/remove.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { SlashCommandBuilder, ActionRowBuilder, StringSelectMenuBuilder, CommandInteraction, User } from "discord.js"; -import {BaseCommand, ExtendedClient} from "../structure"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -export default class RemoveCommand extends BaseCommand { - public static data: SlashCommandBuilder = new SlashCommandBuilder() - .setName("remove") - .setDescription("Remove someone from the ticket"); - constructor(client: ExtendedClient) { - super(client); - } - - async execute(interaction: CommandInteraction) { - const ticket = await this.client.prisma.tickets.findUnique({ - select: { - invited: true, - }, - where: { - channelid: interaction.channel?.id - } - }); - if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); - - const parseInvited = JSON.parse(ticket.invited) as string[]; - if (parseInvited.length < 1) return interaction.reply({ content: "There are no users to remove", ephemeral: true }).catch((e) => console.log(e)); - - const addedUsers: User[] = []; - for (let i = 0; i < parseInvited.length; i++) { - addedUsers.push(await this.client.users.fetch(parseInvited[i])); - } - - const row = new ActionRowBuilder().addComponents( - new StringSelectMenuBuilder() - .setCustomId("removeUser") - .setPlaceholder("Please select a user to remove") - .setMinValues(1) - .setMaxValues(parseInvited.length) - .addOptions( - // @TODO: Fix type definitions when I figure it out via ORM migration. For now assign a random type that gets the error removed. - addedUsers.map((user) => { - return { - label: user.tag, - value: user.id, - }; - }) - ) - ); - await interaction.reply({ components: [row] }).catch((e) => console.log(e)); } -} - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/commands/rename.ts b/src/commands/rename.ts deleted file mode 100644 index 986728ee..00000000 --- a/src/commands/rename.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CommandInteraction, GuildMember, SlashCommandBuilder, TextChannel } from "discord.js"; -import {BaseCommand, ExtendedClient, TicketType} from "../structure"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -export default class RenameCommand extends BaseCommand { - public static data: SlashCommandBuilder = new SlashCommandBuilder() - .setName("rename") - .setDescription("Rename the ticket") - .addStringOption((option) => option.setName("name").setDescription("The new name of the ticket").setRequired(true)); - constructor(client: ExtendedClient) { - super(client); - } - - async execute(interaction: CommandInteraction) { - const ticket = await this.client.prisma.tickets.findUnique({ - where: { - channelid: interaction.channel?.id - } - }); - // @TODO: Breaking change refactor happens here as well.. - const ticketType = ticket ? JSON.parse(ticket.category) as TicketType : undefined; - - if (!ticket) return interaction.reply({ content: "Ticket not found", ephemeral: true }).catch((e) => console.log(e)); - if (!(interaction.member as GuildMember | null)?.roles.cache.some((r) => this.client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || - ticketType?.staffRoles?.includes(r.id))) - return interaction - .reply({ - content: this.client.locales.getValue("ticketOnlyRenamableByStaff"), - ephemeral: true, - }) - .catch((e) => console.log(e)); - - (interaction.channel as TextChannel)?.setName(interaction.options.get("name", true).value as string).catch((e) => console.log(e)); - interaction - .reply({ content: this.client.locales.getValue("ticketRenamed").replace("NEWNAME", (interaction.channel as TextChannel | null)?.toString() ?? "Unknown"), ephemeral: false }) - .catch((e) => console.log(e)); } -} - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 00000000..9b5e97da --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,44 @@ +interface ConfigV0_0_1 { + /** The client ID of the bot application */ + clientId: string; + /** The ID of the Discord server the bot is set up in */ + guildId: string; + /** The lang of the bot */ + lang: "en"; + + /** The ticket types / categories the users can choose */ + ticketTypes: Record< + string, + { + /** The name of the ticket type */ + name: string; + /** The description displayed in the select menu */ + description?: string; + /** The emoji displayed in the select menu. Can be unicode or custom emoji ID. */ + emoji?: string; + /** The ID of the category where the ticket channels will be created. */ + categoryId: string; + /** The name of the ticket channel */ + ticketName: string; + } + >; +} + +interface ConfigVersions { + "0.0.1": ConfigV0_0_1; +} + +type ConfigVersion = keyof ConfigVersions; + +type ConfigOf = ConfigVersions[V]; + +type VersionedConfig = { + version: V; +} & ConfigOf; + +export function defineConfig(version: V, config: ConfigOf): VersionedConfig { + return { + version, + ...config + }; +} diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 00000000..7b50448d --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,27 @@ +import { int, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +// NOTE: Columns that does not have notNull constraint ARE nullable. + +export const ticketsTable = sqliteTable("tickets", { + id: int().primaryKey({ autoIncrement: true }), + /** The ID of the channel where the ticket was created. */ + channelId: text().unique().notNull(), + /** The ID of the message the bot sent when creating the ticket. */ + creationMessageId: text().unique().notNull(), + /** The type of the ticket (ticketType in config). */ + type: text().notNull(), + /** The reason for the ticket, inputted by the user. */ + reason: text(), + /** The user who created the ticket. */ + createdBy: text().notNull(), + /** UNIX time */ + createdAt: int().notNull(), + /** UNIX time */ + claimedAt: int(), + claimedBy: text(), + /** UNIX time */ + closedAt: int(), + closedBy: text(), + closedReason: text(), + transcriptUrl: text() +}); diff --git a/src/events/index.ts b/src/events/index.ts deleted file mode 100644 index 225087f9..00000000 --- a/src/events/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import InteractionCreateEvent from "./interactionCreate"; -import ReadyEvent from "./ready"; - -export { - InteractionCreateEvent, - ReadyEvent -}; \ No newline at end of file diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts deleted file mode 100644 index 3a4d6b21..00000000 --- a/src/events/interactionCreate.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { - ActionRowBuilder, - GuildChannel, - GuildMember, - Interaction, - ModalBuilder, - SelectMenuComponentOptionData, - StringSelectMenuBuilder, - TextInputBuilder, - TextInputStyle, -} from "discord.js"; -import { log } from "../utils/logs"; -import { createTicket } from "../utils/createTicket"; -import { close } from "../utils/close"; -import { claim } from "../utils/claim"; -import { closeAskReason } from "../utils/close_askReason"; -import { deleteTicket } from "../utils/delete"; -import { BaseEvent, ExtendedClient } from "../structure"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -export default class InteractionCreateEvent extends BaseEvent { - constructor(client: ExtendedClient) { - super(client); - } - - public async execute(interaction: Interaction): Promise { - if (interaction.isChatInputCommand()) { - const command = this.client.commands.get(interaction.commandName); - if (!command) return; - - try { - await command.execute(interaction); - } catch (error) { - console.error(error); - await interaction.reply({ - content: "There was an error while executing this command!", - ephemeral: true, - }); - } - } - - if (interaction.isButton()) { - if (interaction.customId === "openTicket") { - await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); - - const tCount = this.client.config.ticketTypes.length; - if (tCount === 0 || tCount > 25) { - await interaction.followUp({ content: this.client.locales.getValue("invalidConfig"), ephemeral: true }); - throw new Error("ticketTypes either has nothing or exceeded 25 entries. Please check the config and restart the bot"); - } - - for (const role of this.client.config.rolesWhoCanNotCreateTickets) { - if (role && (interaction.member as GuildMember | null)?.roles.cache.has(role)) { - interaction - .editReply({ - content: "You can't create a ticket because you are blacklisted", - }) - .catch((e) => console.log(e)); - return; - } - } - - // Max Ticket - if (this.client.config.maxTicketOpened > 0) { - const ticketsOpened = ( - await this.client.prisma.$queryRaw< - [{ count: bigint }] - >`SELECT COUNT(*) as count FROM tickets WHERE closedby IS NULL AND creator = ${interaction.user.id}` - )[0].count; - - // If maxTicketOpened is 0, it means that there is no limit - if (ticketsOpened >= this.client.config.maxTicketOpened) { - interaction - .editReply({ - content: this.client.locales.getValue("ticketLimitReached").replace("TICKETLIMIT", this.client.config.maxTicketOpened.toString()), - }) - .catch((e) => console.log(e)); - return; - } - } - - // Make a select menus of all tickets types - let options: SelectMenuComponentOptionData[] = []; - - for (const x of this.client.config.ticketTypes) { - // x.cantAccess is an array of roles id - // If the user has one of the roles, he can't access to this ticket type - - const a: SelectMenuComponentOptionData = { - label: x.name, - value: x.codeName, - }; - if (x.description) a.description = x.description; - if (x.emoji) a.emoji = x.emoji; - options.push(a); - } - - for (const x of options) { - const option = this.client.config.ticketTypes.filter((y) => y.codeName === x.value)[0]; - if (option.cantAccess) { - for (const role of option.cantAccess) { - if (role && (interaction.member as GuildMember | null)?.roles.cache.has(role)) { - options = options.filter((y) => y.value !== x.value); - } - } - } - } - - if (options.length <= 0) { - interaction.editReply({ - content: this.client.locales.getValue("noTickets"), - }); - return; - } - - const row = new ActionRowBuilder().addComponents( - new StringSelectMenuBuilder() - .setCustomId("selectTicketType") - .setPlaceholder(this.client.locales.getSubValue("other", "selectTicketTypePlaceholder")) - .setMaxValues(1) - .addOptions(options), - ); - - interaction - .editReply({ - components: [row], - }) - .catch((e) => console.log(e)); - } - - if (interaction.customId === "claim") { - claim(interaction, this.client); - } - - if (interaction.customId === "close") { - await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); - close(interaction, this.client, this.client.locales.getSubValue("other", "noReasonGiven"), this.client.config.closeOption.deleteTicket); - } - - if (interaction.customId === "close_askReason") { - closeAskReason(interaction, this.client, this.client.config.closeOption.deleteTicket); - } - - if (interaction.customId === "deleteTicket") { - deleteTicket(interaction, this.client); - } - } - - if (interaction.isStringSelectMenu()) { - if (interaction.customId === "selectTicketType") { - if (this.client.config.maxTicketOpened > 0) { - const ticketsOpened = ( - await this.client.prisma.$queryRaw< - [{ count: bigint }] - >`SELECT COUNT(*) as count FROM tickets WHERE closedby IS NULL AND creator = ${interaction.user.id}` - )[0].count; - // If maxTicketOpened is 0, it means that there is no limit - if (ticketsOpened >= this.client.config.maxTicketOpened) { - interaction - .reply({ - content: this.client.locales.getValue("ticketLimitReached").replace("TICKETLIMIT", this.client.config.maxTicketOpened.toString()), - ephemeral: true, - }) - .catch((e) => console.log(e)); - return; - } - } - - const ticketType = this.client.config.ticketTypes.find((x) => x.codeName === interaction.values[0]); - if (!ticketType) return console.error(`Ticket type ${interaction.values[0]} not found!`); - if (ticketType.askQuestions) { - // Sanity Check - const qCount = ticketType.questions.length; - if (qCount === 0 || qCount > 5) - throw new Error(`${ticketType.codeName} has either no questions or exceeded 5 questions. Check your config and restart the bot`); - - const modal = new ModalBuilder().setCustomId("askReason").setTitle(this.client.locales.getSubValue("modals", "reasonTicketOpen", "title")); - for (const question of ticketType.questions) { - const index = ticketType.questions.indexOf(question); - const input = new TextInputBuilder() - .setCustomId(`input_${interaction.values[0]}_${index}`) - .setLabel(question.label) - .setStyle(question.style == "SHORT" ? TextInputStyle.Short : TextInputStyle.Paragraph) - .setPlaceholder(question.placeholder) - .setMaxLength(question.maxLength); - - const firstActionRow = new ActionRowBuilder().addComponents(input); - modal.addComponents(firstActionRow); - } - - await interaction.showModal(modal).catch((e) => console.log(e)); - } else { - createTicket(interaction, this.client, ticketType, this.client.locales.getSubValue("other", "noReasonGiven")); - } - } - - if (interaction.customId === "removeUser") { - const ticket = await this.client.prisma.tickets.findUnique({ - select: { - id: true, - invited: true, - }, - where: { - channelid: interaction.message.channelId, - }, - }); - for (const value of interaction.values) { - await (interaction.channel as GuildChannel | null)?.permissionOverwrites.delete(value).catch((e) => console.log(e)); - await log( - { - LogType: "userRemoved", - user: interaction.user, - ticketId: ticket?.id.toString(), - ticketChannelId: interaction.channel?.id, - target: { - id: value, - }, - }, - this.client, - ); - } - - // Update the data in the database - await this.client.prisma.tickets.update({ - data: { - invited: JSON.stringify( - (JSON.parse(ticket?.invited ?? "[]") as string[]).filter((userid) => interaction.values.find((rUID) => rUID === userid) === undefined), - ), - }, - where: { - channelid: interaction.channel?.id, - }, - }); - - await interaction.update({ - content: `> Removed ${interaction.values.length < 1 ? interaction.values : interaction.values.map((a) => `<@${a}>`).join(", ")} from the ticket`, - components: [], - }); - } - } - - if (interaction.isModalSubmit()) { - if (interaction.customId === "askReason") { - const type = interaction.fields.fields.first()?.customId.split("_")[1]; - const ticketType = this.client.config.ticketTypes.find((x) => x.codeName === type); - // Using customId until the value can be figured out - if (!ticketType) return console.error(`Ticket type ${interaction.customId} not found!`); - createTicket(interaction, this.client, ticketType, interaction.fields.fields); - } - - if (interaction.customId === "askReasonClose") { - await interaction.deferReply().catch((e) => console.log(e)); - close(interaction, this.client, interaction.fields.fields.first()?.value); - } else if (interaction.customId === "askReasonDelete") { - await interaction.deferReply().catch((e) => console.log(e)); - close(interaction, this.client, interaction.fields.fields.first()?.value, true); - } - } - } -} - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/events/ready.ts b/src/events/ready.ts deleted file mode 100644 index 86d529ff..00000000 --- a/src/events/ready.ts +++ /dev/null @@ -1,307 +0,0 @@ -import readline from "readline"; -import axios from "axios"; -import {client as WebSocketClient, connection} from "websocket"; -import {ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ColorResolvable, EmbedBuilder, Message} from "discord.js"; -import os from "os"; -import {BaseEvent, ExtendedClient, SponsorType} from "../structure"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -export default class ReadyEvent extends BaseEvent { - private connected = false; - constructor(client: ExtendedClient) { - super(client); - } - - public async execute() { - if (!this.client.config.guildId) { - console.log("⚠️⚠️⚠️ Please add the guild id in the config.jsonc file. ⚠️⚠️⚠️"); - process.exit(0); - } - - await this.client.guilds.fetch(this.client.config.guildId); - await this.client.guilds.cache.get(this.client.config.guildId)?.members.fetch(); - if (!this.client.guilds.cache.get(this.client.config.guildId)?.members.me?.permissions.has("Administrator")) { - console.log("\n⚠️⚠️⚠️ I don't have the Administrator permission, to prevent any issues please add the Administrator permission to me. ⚠️⚠️⚠️"); - process.exit(0); - } - - const embedMessageId = (await this.client.prisma.config.findUnique({ - where: { - key: "openTicketMessageId", - } - }))?.value; - await this.client.channels.fetch(this.client.config.openTicketChannelId).catch(() => { - console.error("The channel to open tickets is not found!"); - process.exit(0); - }); - const openTicketChannel = await this.client.channels.cache.get(this.client.config.openTicketChannelId); - if (!openTicketChannel) { - console.error("The channel to open tickets is not found!"); - process.exit(0); - } - - if (!openTicketChannel.isTextBased()) { - console.error("The channel to open tickets is not a channel!"); - process.exit(0); - } - const locale = this.client.locales; - let footer = locale.getSubValue("embeds", "openTicket", "footer", "text").replace("ticket.pm", ""); - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - footer = `ticket.pm ${footer.trim() !== "" ? `- ${footer}` : ""}`; // Please respect the LICENSE :D - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - const embed = new EmbedBuilder({ - ...locale.getSubRawValue("embeds.openTicket") as object, - color: 0, - }) - .setColor( - locale.getNoErrorSubValue("embeds", "openTicket", "color") as ColorResolvable | undefined ?? - this.client.config.mainColor - ) - .setFooter({ - text: footer, - iconURL: locale.getNoErrorSubValue("embeds.openTicket.footer.iconURL") - }); - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId("openTicket").setLabel(this.client.locales.getSubValue("other", "openTicketButtonMSG")).setStyle(ButtonStyle.Primary) - ); - - try { - // Fetch Message object and return undefined if not found - const msg = embedMessageId ? await (()=> new Promise((res)=> { - openTicketChannel?.messages?.fetch(embedMessageId) - .then(msg=>res(msg)) - .catch(()=>res(undefined)); - }))() : undefined; - - if (msg && msg.id) { - msg.edit({ - embeds: [embed], - components: [row] - }); - } else { - const channel = this.client.channels.cache.get(this.client.config.openTicketChannelId); - if(channel?.type !== ChannelType.GuildText) - return console.error("Invalid openTicketChannelId"); - channel.send({ - embeds: [embed], - components: [row] - }).then((rMsg) => { - this.client.prisma.config.upsert({ - create: { - key: "openTicketMessageId", - value: rMsg.id - }, - update: { - value: rMsg.id - }, - where: { - key: "openTicketMessageId" - } - }).then(); // I need .then() for it to execute?!?!?? - }); - } - } catch (e) { - console.error(e); - } - - - this.setStatus(); - setInterval(()=>this.setStatus(), 9e5); // 15 minutes - - readline.cursorTo(process.stdout, 0); - process.stdout.write( - `\x1b[0m🚀 The bot is ready! Logged in as \x1b[37;46;1m${this.client.user?.tag}\x1b[0m (\x1b[37;46;1m${this.client.user?.id}\x1b[0m) - \x1b[0m🌟 You can leave a star on GitHub: \x1b[37;46;1mhttps://github.com/Sayrix/ticket-bot \x1b[0m - \x1b[0m⛅ Host your ticket-bot by being a sponsor from 1$/month: \x1b[37;46;1mhttps://github.com/sponsors/Sayrix \x1b[0m\n`.replace(/\t/g, "") - ); - - const a = await axios.get("https://raw.githubusercontent.com/Sayrix/sponsors/main/sponsors.json").catch(() => {return;}); - if (a) { - const sponsors: SponsorType[] = a.data; - const sponsorsList = sponsors - .map((s) => `\x1b]8;;https://github.com/${s.sponsor.login}\x1b\\\x1b[1m${s.sponsor.login}\x1b]8;;\x1b\\\x1b[0m`) - .join(", "); - process.stdout.write(`\x1b[0m💖 Thanks to our sponsors: ${sponsorsList}\n`); - } - - - if ((await this.client.prisma.config.findUnique({ - where: { - key: "firstStart", - } - })) === null) { - await this.client.prisma.config.create({ - data: { - key: "firstStart", - value: "true", - } - }); - - if(!this.client.config.minimalTracking) console.warn(` - PRIVACY NOTICES - ------------------------------- - Telemetry is current set to full and the following information are sent to the server anonymously: - * Discord Bot's number of guilds & users - * Current Source Version - * NodeJS Version - * OS Version - * CPU version, name, core count, architecture, and model - * Current Process up-time - * System total ram and freed ram - * Client name and id - * Guild ID - ------------------------------- - If you wish to minimize the information that are being sent, please set "minimalTracking" to true in the config - `.replace(/\t/g, "")); - else console.warn(` - PRIVACY NOTICES - ------------------------------- - Minimal tracking has been enabled; the following information are sent anonymously: - * Current Source Version - * NodeJS Version - ------------------------------- - `.replace(/\t/g, "")); - } - - this.connect(this.client.config.showWSLog); - - this.client.deployCommands(); - } - - private setStatus(): void { - if (this.client.config.status) { - if (!this.client.config.status.enabled) return; - - let type = 0; - switch(this.client.config.status.type) { - case "PLAYING": - type = 0; - break; - case "STREAMING": - type = 1; - break; - case "LISTENING": - type = 2; - break; - case "WATCHING": - type = 3; - break; - case "CUSTOM": - type = 4; - break; - case "COMPETING": - type = 5; - break; - } - - if (this.client.config.status.type && this.client.config.status.text) { - // If the user just want to set the status but not the activity - const url = this.client.config.status.url; - this.client.user?.setPresence({ - activities: [{ name: this.client.config.status.text, type: type, url: (url && url.trim() !== "") ? url : undefined }], - status: this.client.config.status.status, - }); - } - this.client.user?.setStatus(this.client.config.status.status); - } - } - - private connect(enableLog?: boolean): void { - if (this.connected) return; - const ws = new WebSocketClient(); - ws.on("connectFailed", (e) => { - this.connected = false; - setTimeout(()=>this.connect(enableLog), Math.random() * 1e4); - if(enableLog) - console.log(`❌ WebSocket Error: ${e.toString()}`); - }); - - ws.on("connect", (connection) => { - connection.on("error", (e) => { - this.connected = false; - setTimeout(()=>this.connect(enableLog), Math.random() * 1e4); - if(enableLog) - console.log(`❌ WebSocket Error: ${e.toString()}`); - }); - - connection.on("close", (e) => { - this.connected = false; - setTimeout(()=>this.connect(enableLog), Math.random() * 1e4); - if(enableLog) - console.log(`❌ WebSocket Error: ${e.toString()}`); - }); - - this.connected = true; - if(enableLog) - console.log("✅ Connected to WebSocket server."); - this.telemetry(connection); - - setInterval(() => { - this.telemetry(connection); - }, 120_000); - }); - - ws.connect("wss://ws.ticket.pm", "echo-protocol"); - - } - - private telemetry(connection: connection) { - let fullInfo: {[key:string]: string | number | {[key:string]: string | number}} = { - os: os.platform(), - osVersion1: os.release(), - osVersion2: os.version(), - uptime: process.uptime(), - ram: { - total: os.totalmem(), - free: os.freemem() - }, - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length, - arch: os.arch() - } - }; - let moreInfo: {[key:string]: string | undefined} = { - clientName: this.client?.user?.tag, - clientId: this.client?.user?.id, - guildId: this.client?.config?.guildId - }; - // Minimal tracking enabled, remove those info from being sent - if(this.client.config.minimalTracking) { - fullInfo = {}; - moreInfo = {}; - } - connection.sendUTF( - JSON.stringify({ - type: "telemetry", - data: { - stats: { - guilds: this.client?.guilds?.cache?.size, - users: this.client?.users?.cache?.size - }, - infos: { - // eslint-disable-next-line @typescript-eslint/no-var-requires - ticketbotVersion: require("../../package.json").version, - nodeVersion: process.version, - ...fullInfo - }, - ...moreInfo - } - }) - ); - } -} - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/index.ts b/src/index.ts index f6bfde65..375039e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,79 +1,4 @@ -/* -Copyright 2023 Sayrix (github.com/Sayrix) +import "dotenv/config"; +import { drizzle } from "drizzle-orm/libsql"; -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -import fs from "fs-extra"; -import path from "node:path"; -import { GatewayIntentBits } from "discord.js"; -import { jsonc } from "jsonc"; -import { config as envconf } from "dotenv"; -import {ConfigType, ExtendedClient} from "./structure"; - -// Initalize .env file as environment -try {envconf();} -catch {console.log(".env failed to load");} - -// Although invalid type, it should be good enough for now until more stuff needs to be handled here -process.on("unhandledRejection", (reason: string, promise: string, a: string) => { - console.error(reason, promise, a); -}); - -process.on("uncaughtException", (err: string) => { - console.error(err); -}); - -process.stdout.write(` -\x1b[38;2;143;110;250m████████╗██╗ ██████╗██╗ ██╗███████╗████████╗ ██████╗ ██████╗ ████████╗ -\x1b[38;2;157;101;254m╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝ ██╔══██╗██╔═══██╗╚══██╔══╝ -\x1b[38;2;172;90;255m ██║ ██║██║ █████╔╝ █████╗ ██║ ██████╔╝██║ ██║ ██║ -\x1b[38;2;188;76;255m ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ██╔══██╗██║ ██║ ██║ -\x1b[38;2;205;54;255m ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ██████╔╝╚██████╔╝ ██║ -\x1b[38;2;222;0;255m ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝\x1b[0m - https://github.com/Sayrix/ticket-bot - -Connecting to Discord... -`); - -// Update Detector -fetch("https://api.github.com/repos/Sayrix/Ticket-Bot/tags").then((res) => { - if (Math.floor(res.status / 100) !== 2) return console.warn("🔄 Failed to pull latest version from server"); - res.json().then((json) => { - // Assumign the format stays consistent (i.e. x.x.x) - const latest = json[0].name.split(".").map((k: string) => parseInt(k)); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const current = require("../package.json").version.split(".") - .map((k: string) => parseInt(k)); - if ( - latest[0] > current[0] || - (latest[0] === current[0] && latest[1] > current[1]) || - (latest[0] === current[0] && latest[1] === current[1] && latest[2] > current[2]) - ) - console.warn(`🔄 New version available: ${json[0].name}; Current Version: ${current.join(".")}`); - else console.log("🔄 The ticket-bot is up to date"); - }); -}); - -const config: ConfigType = jsonc.parse(fs.readFileSync(path.join(__dirname, "/../config/config.jsonc"), "utf8")); - -const client = new ExtendedClient({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers], - presence: { - status: config.status?.status ?? "online" - } -}, config); - -// Login the bot -const token = process.env["TOKEN"]; -if(!token || token.trim() === "") - throw new Error("TOKEN Environment Not Found"); -client.login(process.env["TOKEN"]).then(null); - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ +const db = drizzle(process.env.DB_FILE_NAME); diff --git a/src/structure/BaseCommand.ts b/src/structure/BaseCommand.ts deleted file mode 100644 index 1ef13c65..00000000 --- a/src/structure/BaseCommand.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {CommandInteraction, InteractionResponse, SlashCommandBuilder} from "discord.js"; -import {ExtendedClient} from "./"; - -export default abstract class BaseCommand { - public static data: SlashCommandBuilder; - protected client: ExtendedClient; - protected constructor(client: ExtendedClient) { - this.client = client; - } - // eslint-disable-next-line no-unused-vars - abstract execute(interaction: CommandInteraction): void | Promise>; - -} \ No newline at end of file diff --git a/src/structure/BaseEvent.ts b/src/structure/BaseEvent.ts deleted file mode 100644 index 6926dd08..00000000 --- a/src/structure/BaseEvent.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {ExtendedClient} from "./"; -import {ClientEvents} from "discord.js"; - -export default abstract class BaseEvent { - protected readonly client: ExtendedClient; - protected constructor(client: ExtendedClient) { - this.client = client; - } - - // eslint-disable-next-line no-unused-vars - protected abstract execute(...args: ClientEvents[keyof ClientEvents]): void | Promise; - -} \ No newline at end of file diff --git a/src/structure/ExtendedClient.ts b/src/structure/ExtendedClient.ts deleted file mode 100644 index fd1fa8a8..00000000 --- a/src/structure/ExtendedClient.ts +++ /dev/null @@ -1,85 +0,0 @@ -import {Client, ClientOptions, Collection, Routes} from "discord.js"; -import {BaseCommand, ConfigType} from "./"; -import {PrismaClient} from "@prisma/client"; -import fs from "fs-extra"; -import path from "node:path"; -import {AddCommand, MassAddCommand, ClaimCommand, CloseCommand, RemoveCommand, RenameCommand, clearDM} from "../commands"; -import {InteractionCreateEvent, ReadyEvent} from "../events"; -import {jsonc} from "jsonc"; -import {REST} from "@discordjs/rest"; -import {Translation} from "../utils/translation"; - -export default class ExtendedClient extends Client { - public readonly config: ConfigType; - public readonly prisma: PrismaClient; - public locales: Translation; - public commands: Collection; - constructor(options: ClientOptions, config: ConfigType) { - super(options); - - this.config = config; - this.prisma = new PrismaClient({ - errorFormat: "minimal" - }); - this.locales = new Translation(this.config.lang, path.join(__dirname, "../../locales/")); - this.commands = new Collection([ - [AddCommand.data.name, new AddCommand(this)], - [MassAddCommand.data.name, new MassAddCommand(this)], - [ClaimCommand.data.name, new ClaimCommand(this)], - [CloseCommand.data.name, new CloseCommand(this)], - [RemoveCommand.data.name, new RemoveCommand(this)], - [RenameCommand.data.name, new RenameCommand(this)], - [clearDM.data.name, new clearDM(this)], - ]); - this.loadEvents(); - - } - - public msToHm (ms: number | Date) { - - if(ms instanceof Date) ms = ms.getTime(); - - const days = Math.floor(ms / (24 * 60 * 60 * 1000)); - const daysms = ms % (24 * 60 * 60 * 1000); - const hours = Math.floor(daysms / (60 * 60 * 1000)); - const hoursms = ms % (60 * 60 * 1000); - const minutes = Math.floor(hoursms / (60 * 1000)); - const minutesms = ms % (60 * 1000); - const sec = Math.floor(minutesms / 1000); - - let result = "0s"; - - if (days > 0) result = `${days}d ${hours}h ${minutes}m ${sec}s`; - if (hours > 0) result = `${hours}h ${minutes}m ${sec}s`; - if (minutes > 0) result = `${minutes}m ${sec}s`; - if (sec > 0) result = `${sec}s`; - return result; - - } - - private loadEvents () { - this.on("interactionCreate", (interaction) => new InteractionCreateEvent(this).execute(interaction)); - this.on("ready", () => new ReadyEvent(this).execute()); - } - - public deployCommands() { - const commands = [ - AddCommand.data.toJSON(), - ClaimCommand.data.toJSON(), - CloseCommand.data.toJSON(), - RemoveCommand.data.toJSON(), - RenameCommand.data.toJSON(), - clearDM.data.toJSON(), - ]; - - const { guildId } = jsonc.parse(fs.readFileSync(path.join(__dirname, "../../config/config.jsonc"), "utf8")); - - if(!process.env["TOKEN"]) throw Error("Discord Token Expected, deploy-command"); - const rest = new REST({ version: "10" }).setToken(process.env["TOKEN"]); - - rest - .put(Routes.applicationGuildCommands(this.user?.id ?? "", guildId), { body: commands }) - .then(() => console.log("✅ Successfully registered application commands.")) - .catch(console.error); - } -} diff --git a/src/structure/index.ts b/src/structure/index.ts deleted file mode 100644 index cae6ea13..00000000 --- a/src/structure/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import BaseCommand from "./BaseCommand"; -import BaseEvent from "./BaseEvent"; -import ExtendedClient from "./ExtendedClient"; - -export * from "./types"; -export { - BaseCommand, - BaseEvent, - ExtendedClient, -}; \ No newline at end of file diff --git a/src/structure/types.ts b/src/structure/types.ts deleted file mode 100644 index a8e379d2..00000000 --- a/src/structure/types.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* Licensed under Apache License 2.0: https://github.com/Sayrix/Ticket-Bot/blob/typescript/LICENSE */ - -import { ColorResolvable } from "discord.js"; - -export type ConfigType = { - clientId: string; - guildId: string; - mainColor: ColorResolvable; - lang: string; // Tho can be cs/de/es/fr/main/tr type but we can't guarantee what users put - closeTicketCategoryId: string; - openTicketChannelId: string; - ticketTypes: TicketType[]; - ticketNameOption: string; - claimOption: { - claimButton: boolean; - nameWhenClaimed?: string; - categoryWhenClaimed?: string; - }; - rolesWhoHaveAccessToTheTickets: string[]; - rolesWhoCanNotCreateTickets: string[]; - pingRoleWhenOpened: boolean; - roleToPingWhenOpenedId: string[]; - logs: boolean; - logsChannelId: string; - closeOption: { - closeButton: boolean; - dmUser: boolean; - createTranscript: boolean; - askReason: boolean; - whoCanCloseTicket: "STAFFONLY" | "EVERYONE"; - closeTicketCategoryId?: string; - deleteTicket: boolean; - }; - uuidType: "uuid" | "emoji"; - status: { - enabled: boolean; - text: string; - type: "PLAYING" | "STREAMING" | "LISTENING" | "WATCHING" | "COMPETING" | "CUSTOM"; - url?: string; - status: "online"; - }; - maxTicketOpened: number; - minimalTracking: boolean; - showWSLog: boolean; -}; - -export type LocaleType = { - embeds: { - openTicket: { - title: string; - color?: ColorResolvable; - description: string; - footer: { - text: string; - }; - }; - ticketOpened: { - title: string; - description: string; - footer: { - text: string; - iconUrl?: string; - }; - }; - ticketClosed: { - title: string; - description: string; - }; - ticketClosedDM: { - title: string; - color?: ColorResolvable; - description: string; - footer: { - text: string; - iconUrl?: string; - }; - }; - }; - modals: { - reasonTicketOpen: { - title: string; - label: string; - placeholder: string; - }; - reasonTicketClose: { - title: string; - label: string; - placeholder: string; - }; - }; - buttons: { - close: { - label: string; - emoji: string; - }; - claim: { - label: string; - emoji: string; - }; - }; - invalidConfig: string; - ticketOpenedMessage: string; - ticketOnlyClaimableByStaff: string; - ticketAlreadyClaimed: string; - ticketClaimedMessage: string; - ticketOnlyClosableByStaff: string; - ticketOnlyRenamableByStaff: string; - ticketRenamed: string; - noTickets: string; - ticketAlreadyClosed: string; - ticketCreatingTranscript: string; - ticketTranscriptCreated: string; - ticketLimitReached: string; - - other: { - openTicketButtonMSG: string; - deleteTicketButtonMSG: string; - selectTicketTypePlaceholder: string; - claimedBy: string; - noReasonGiven: string; - unavailable: string; - }; -}; - -export type SponsorType = { - sponsor: { - login: string; - name: string; - avatarUrl: string; - websiteUrl?: string; - linkUrl: string; - type: string; - avatarUrlHighRes: string; - avatarUrlMediumRes: string; - avatarUrlLowRes: string; - }; - isOneTime: boolean; - monthlyDollars: number; - privacyLevel: string; - tierName: string; - createdAt: string; - provider: string; -}; - -// Config types and setups -type TicketQuestionType = { - label: string; - placeholder: string; - style: string; - maxLength: number; -}; - -export type TicketType = { - codeName: string; - name: string; - description: string; - emoji: string; - color?: ColorResolvable; - categoryId: string; - ticketNameOption: string; - customDescription: string; - cantAccess: string[]; - askQuestions: boolean; - questions: TicketQuestionType[]; - staffRoles?: string[]; -}; diff --git a/src/types/config.d.ts b/src/types/config.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/types/process.d.ts b/src/types/process.d.ts new file mode 100644 index 00000000..b4a0eb92 --- /dev/null +++ b/src/types/process.d.ts @@ -0,0 +1,10 @@ +export {}; + +declare global { + namespace NodeJS { + interface ProcessEnv extends Record { + DB_FILE_NAME: string; + DISCORD_TOKEN: string; + } + } +} diff --git a/src/utils/claim.ts b/src/utils/claim.ts deleted file mode 100644 index bb1cc721..00000000 --- a/src/utils/claim.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -import { APIButtonComponent, ActionRow, ActionRowBuilder, ButtonBuilder, ButtonInteraction, ChannelType, CommandInteraction, EmbedBuilder, GuildMember, MessageActionRowComponent } from "discord.js"; -import { log } from "./logs"; -import {ExtendedClient, TicketType} from "../structure"; - -export const claim = async(interaction: ButtonInteraction | CommandInteraction, client: ExtendedClient) => { - // Channel Sanity Checks to keep the code smaller ig? - if(!interaction.channel || interaction.channel.type !== ChannelType.GuildText) - return await interaction.reply({ - content: "This command can only be used in a ticket channel.", - ephemeral: true - }); - - let ticket = await client.prisma.tickets.findUnique({ - where: { - channelid: interaction.channel.id - } - }); - const claimed = ticket?.claimedat && ticket.claimedby; - - if (!ticket) - return interaction.reply({ - content: "Ticket not found", - ephemeral: true, - }); - - // @TODO: Breaking change refactor happens here as well.. - const ticketType = ticket ? JSON.parse(ticket.category) as TicketType : undefined; - const canClaim = (interaction.member as GuildMember | null)?.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || - ticketType?.staffRoles?.includes(r.id)); - - if (!canClaim) - return interaction - .reply({ - content: client.locales.getValue("ticketOnlyClaimableByStaff"), - ephemeral: true, - }) - .catch((e) => console.log(e)); - - if (claimed) - return interaction - .reply({ - content: client.locales.getValue("ticketAlreadyClaimed"), - ephemeral: true, - }) - .catch((e) => console.log(e)); - - log( - { - LogType: "ticketClaim", - user: interaction.user, - ticketId: ticket.id.toString(), - ticketChannelId: interaction.channel.id, - ticketCreatedAt: ticket.createdat, - }, - client - ); - - ticket = await client.prisma.tickets.update({ - data: { - claimedby: interaction.user.id, - claimedat: Date.now() - }, - where: { - channelid: interaction.channel.id, - } - }); - - const msg = await interaction.channel.messages.fetch(ticket.messageid); - const oldEmbed = msg?.embeds[0].data; - const newEmbed = new EmbedBuilder(oldEmbed) - .setDescription(oldEmbed?.description + `\n\n ${client.locales.getSubValue("other", "claimedBy").replace("USER", `<@${interaction.user.id}>`)}`); - - const row = new ActionRowBuilder(); - (msg?.components[0] as ActionRow)?.components.map((x) => { - const btnBuilder = new ButtonBuilder(x.data as APIButtonComponent); - if (x.customId === "claim") btnBuilder.setDisabled(true); - row.addComponents(btnBuilder); - }); - msg?.edit({ - content: msg.content, - embeds: [newEmbed], - components: [row], - }).catch((e) => console.log(e)); - - interaction - .reply({ - content: client.locales.getValue("ticketClaimedMessage").replace("USER", `<@${interaction.user.id}>`), - ephemeral: false, - }) - .catch((e) => console.log(e)); - - const defaultName = client.config.claimOption.nameWhenClaimed; - if (defaultName && defaultName.trim() !== "") { - const creatorUser = await client.users.fetch(ticket.creator); - const newName = defaultName - .replaceAll("S_USERNAME", interaction.user.username) - .replaceAll("U_USERNAME", creatorUser.username) - .replaceAll("S_USERID", interaction.user.id) - .replaceAll("U_USERID", creatorUser.id) - .replaceAll("TICKETCOUNT", ticket.id.toString()); - await interaction.channel.setName(newName).catch((e) => console.log(e)); - } - - // Move to claimed category when ticket is claimed - const categoryID = client.config.claimOption.categoryWhenClaimed; - if(categoryID && categoryID.trim() !== "") { - const category = await interaction.guild?.channels.fetch(categoryID); - if(category?.type !== ChannelType.GuildCategory) - return console.error("claim.ts: USER ERROR - Invalid categoryWhenClaimed ID. Channel must be a category."); - const oldPerm = interaction.channel.permissionOverwrites.cache; - await interaction.channel.setParent(category); - if(oldPerm) - await interaction.channel.permissionOverwrites.set(oldPerm); - } -}; -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/utils/close.ts b/src/utils/close.ts deleted file mode 100644 index 5f6f7331..00000000 --- a/src/utils/close.ts +++ /dev/null @@ -1,307 +0,0 @@ -import axios from "axios"; -import { - ActionRow, - ActionRowBuilder, - ButtonBuilder, - ButtonInteraction, - ButtonStyle, - ChannelType, - Collection, - ColorResolvable, - CommandInteraction, - ComponentType, - EmbedBuilder, - GuildMember, - Message, - MessageActionRowComponent, - ModalSubmitInteraction, - TextChannel -} from "discord.js"; -import { generateMessages } from "ticket-bot-transcript-uploader"; -import zlib from "zlib"; -import { ExtendedClient, TicketType } from "../structure"; -import { log } from "./logs"; -let domain = "https://ticket.pm/"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -type ticketType = { - id: number; - channelid: string; - messageid: string; - category: string; - invited: string; - reason: string; - creator: string; - createdat: bigint; - claimedby: string | null; - claimedat: bigint | null; - closedby: string | null; - closedat: bigint | null; - closereason: string | null; - transcript: string | null; -}; - -export async function close( - interaction: ButtonInteraction | CommandInteraction | ModalSubmitInteraction, - client: ExtendedClient, - reason?: string, - deleteTicket: boolean = false -) { - - if(!interaction.channel || interaction.channel.type !== ChannelType.GuildText) - return await interaction.reply({ - content: "This command can only be used in a ticket channel.", - ephemeral: true - }); - - if (!client.config.closeOption.createTranscript) domain = client.locales.getSubValue("other", "unavailable"); - - const ticket = await client.prisma.tickets.findUnique({ - where: { - channelid: interaction.channel.id - } - }); - const ticketClosed = ticket?.closedat && ticket.closedby; - if (!ticket) return interaction.editReply({ content: "Ticket not found" }).catch((e) => console.log(e)); - - // @TODO: Breaking change refactor happens here as well.. - const ticketType = ticket ? (JSON.parse(ticket.category) as TicketType) : undefined; - - if ( - client.config.closeOption.whoCanCloseTicket === "STAFFONLY" && - !(interaction.member as GuildMember | null)?.roles.cache.some( - (r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || ticketType?.staffRoles?.includes(r.id) - ) - ) - return interaction - .editReply({ - content: client.locales.getValue("ticketOnlyClosableByStaff") - }) - .catch((e) => console.log(e)); - - if (ticketClosed) - return interaction - .editReply({ - content: client.locales.getValue("ticketAlreadyClosed") - }) - .catch((e) => console.log(e)); - - log( - { - LogType: "ticketClose", - user: interaction.user, - ticketId: ticket.id, - ticketChannelId: interaction.channel.id, - ticketCreatedAt: ticket.createdat, - reason: reason - }, - client - ); - - // Normally the user that closes the ticket will get posted here, but we'll do it when the ticket finalizes - - const creator = ticket.creator; - const invited = JSON.parse(ticket.invited) as string[]; - - interaction.channel.permissionOverwrites - .edit(creator, { - ViewChannel: false - }) - .catch((e: unknown) => console.log(e)); - for (const user of invited) { - await (interaction.channel as TextChannel | null)?.permissionOverwrites - .edit(user, { - ViewChannel: false - }); - } - - interaction - .editReply({ - content: client.locales.getValue("ticketCreatingTranscript") - }) - .catch((e) => console.log(e)); - async function _close(id: string, ticket: ticketType) { - if (client.config.closeOption.closeTicketCategoryId) - (interaction.channel as TextChannel | null)?.setParent(client.config.closeOption.closeTicketCategoryId).catch((e) => console.log(e)); - - const msg = await interaction.channel?.messages.fetch(ticket.messageid); - const embed = new EmbedBuilder(msg?.embeds[0].data); - - const rowAction = new ActionRowBuilder(); - (msg?.components[0] as ActionRow)?.components?.map((x) => { - if (x.type !== ComponentType.Button) return; - const builder = new ButtonBuilder(x.data); - if (x.customId === "close") builder.setDisabled(true); - if (x.customId === "close_askReason") builder.setDisabled(true); - rowAction.addComponents(builder); - }); - - msg - ?.edit({ - content: msg.content, - embeds: [embed], - components: [rowAction] - }) - .catch((e) => console.log(e)); - - // Workaround for type handling, rewrite should not follow this. - if(interaction.channel && interaction.channel.type !== ChannelType.GuildText) - throw Error("Close util used in a non-text channel"); - - interaction.channel - ?.send({ - content: client.locales - .getValue("ticketTranscriptCreated") - .replace( - "TRANSCRIPTURL", - domain === client.locales.getSubValue("other", "unavailable") ? client.locales.getSubValue("other", "unavailable") : `<${domain}${id}>` - ) - }) - .catch((e) => console.log(e)); - - ticket = await client.prisma.tickets.update({ - data: { - closedby: interaction.user.id, - closedat: Date.now(), - closereason: reason, - transcript: domain === client.locales.getSubValue("other", "unavailable") ? client.locales.getSubValue("other", "unavailable") : `${domain}${id}` - }, - where: { - channelid: interaction.channel?.id - } - }); - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("deleteTicket") - .setLabel(client.locales.getSubValue("other", "deleteTicketButtonMSG")) - .setStyle(ButtonStyle.Danger) - .setDisabled(deleteTicket) - ); - const locale = client.locales; - // Use JSON.stringify().slice(1, -1) to safely escape (strips outer quotes while keeping JSON special chars escaped) - const safeTicketCount = JSON.stringify(ticket.id.toString()).slice(1, -1); - const safeReason = JSON.stringify( - (ticket.closereason ?? client.locales.getSubValue("other", "noReasonGiven")).replace(/[\n\r]/g, "\\n") - ).slice(1, -1); - const safeCloserName = JSON.stringify(interaction.user.tag).slice(1, -1); - interaction.channel - ?.send({ - embeds: [ - JSON.parse( - JSON.stringify(locale.getSubRawValue("embeds", "ticketClosed")) - .replace("TICKETCOUNT", safeTicketCount) - .replace("REASON", safeReason) - .replace("CLOSERNAME", safeCloserName) - ) - ], - components: [row] - }) - .catch((e) => console.log(e)); - - if (deleteTicket) { - log( - { - LogType: "ticketDelete", - user: interaction.user, - ticketId: ticket.id, - ticketCreatedAt: ticket.createdat, - transcriptURL: ticket.transcript ?? undefined - }, - client - ); - - interaction.channel?.send({ - content: client.locales.getSubValue("embeds", "ticketClosed", "deleteTicketInfo") - }); - setTimeout(() => interaction.channel?.delete().catch((e) => console.log(e)), 15000); // ticket will be deleted within 15 seconds - } - - if (!client.config.closeOption.dmUser) return; - const footer = locale.getSubValue("embeds", "ticketClosedDM", "footer", "text").replace("ticket.pm", ""); - const ticketClosedDMEmbed = new EmbedBuilder({ - color: 0 - }) - .setColor((locale.getNoErrorSubValue("embeds", "ticketClosedDM", "color") as ColorResolvable) ?? client.config.mainColor) - .setDescription( - client.locales - .getSubValue("embeds", "ticketClosedDM", "description") - .replace("TICKETCOUNT", ticket.id.toString()) - .replace("TRANSCRIPTURL", `${domain}${id}`) - .replace("REASON", ticket.closereason ?? client.locales.getSubValue("other", "noReasonGiven")) - .replace("CLOSERNAME", interaction.user.tag) - ) - .setFooter({ - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - text: `ticket.pm ${footer.trim() !== "" ? `- ${footer}` : ""}`, // Please respect the LICENSE :D - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - iconURL: locale.getNoErrorSubValue("embeds", "ticketClosedDM", "footer", "iconUrl") - }); - - client.users.fetch(creator).then((user) => { - user - .send({ - embeds: [ticketClosedDMEmbed] - }) - .catch((e) => console.log(e)); - }); - } - - if (!client.config.closeOption.createTranscript) { - _close("", ticket); - return; - } - - async function fetchAll() { - const collArray: Collection>[] = []; - let lastID = (interaction.channel as TextChannel | null)?.lastMessageId; - // eslint-disable-next-line no-constant-condition - while (true) { - // using if statement for this check causes a TypeScript bug. Hard to reproduce; thus, bug report won't be accepted. - if (!lastID) break; - const fetched = await interaction.channel?.messages.fetch({ limit: 100, before: lastID }); - if (fetched?.size === 0) { - break; - } - if (fetched) collArray.push(fetched); - lastID = fetched?.last()?.id; - if (fetched?.size !== 100) { - break; - } - } - const messages = collArray[0].concat(...collArray.slice(1)); - return messages; - } - - const messages = await fetchAll(); - const premiumKey = ""; - - const messagesJSON = await generateMessages(messages, premiumKey, "https://m.ticket.pm"); - zlib.gzip(JSON.stringify(messagesJSON), async (err, compressed) => { - if (err) { - console.error(err); - } else { - const ts = await axios - .post(`${domain}upload?key=${premiumKey}&uuid=${client.config.uuidType}`, JSON.stringify(compressed), { - headers: { - "Content-Type": "application/json" - } - }) - .catch(console.error); - _close(ts?.data, ticket); - } - }); -} - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/utils/close_askReason.ts b/src/utils/close_askReason.ts deleted file mode 100644 index 098b53fe..00000000 --- a/src/utils/close_askReason.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -import { ActionRowBuilder, ButtonInteraction, CommandInteraction, GuildMember, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; -import { ExtendedClient, TicketType } from "../structure"; - -export const closeAskReason = async (interaction: CommandInteraction | ButtonInteraction, client: ExtendedClient, deleteTicket: boolean = false) => { - // @TODO: Breaking change refactor happens here as well.. - const ticket = await client.prisma.tickets.findUnique({ - where: { - channelid: interaction.channel?.id, - }, - }); - const ticketType = ticket ? (JSON.parse(ticket.category) as TicketType) : undefined; - - if ( - client.config.closeOption.whoCanCloseTicket === "STAFFONLY" && - !(interaction.member as GuildMember | null)?.roles.cache.some( - (r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || ticketType?.staffRoles?.includes(r.id), - ) - ) - return interaction - .reply({ - content: client.locales.getValue("ticketOnlyClosableByStaff"), - ephemeral: true, - }) - .catch((e) => console.log(e)); - - const modal = new ModalBuilder().setCustomId(!deleteTicket ? "askReasonClose" : "askReasonDelete").setTitle(client.locales.getSubValue("modals", "reasonTicketClose", "title")); - - const input = new TextInputBuilder() - .setCustomId("reason") - .setLabel(client.locales.getSubValue("modals", "reasonTicketClose", "label")) - .setStyle(TextInputStyle.Paragraph) - .setPlaceholder(client.locales.getSubValue("modals", "reasonTicketClose", "placeholder")) - .setMaxLength(256); - - const firstActionRow = new ActionRowBuilder().addComponents(input); - modal.addComponents(firstActionRow); - await interaction.showModal(modal).catch((e) => console.log(e)); -}; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/utils/createTicket.ts b/src/utils/createTicket.ts deleted file mode 100644 index 59481aa4..00000000 --- a/src/utils/createTicket.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - Collection, - EmbedBuilder, - ModalSubmitInteraction, - PermissionFlagsBits, - StringSelectMenuInteraction, - TextInputComponent -} from "discord.js"; -import { ExtendedClient, TicketType } from "../structure"; -import { log } from "./logs"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -/** - * @param {Discord.Interaction} interaction - * @param {Discord.Client} client - * @param {Object} ticketType - * @param {Object|string} reasons - */ -export const createTicket = async ( - interaction: StringSelectMenuInteraction | ModalSubmitInteraction, - client: ExtendedClient, - ticketType: TicketType, - reasons?: Collection | string -) => { - const locale = client.locales; - // eslint-disable-next-line no-async-promise-executor - return new Promise(async function (resolve, reject) { - await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); - - const reason: string[] = []; - let allReasons = ""; - - if (typeof reasons === "object") { - for (const [, r] of reasons) { - reason.push(r.value); - } - allReasons = reason.map((r, i) => `Question ${i + 1}: ${r}`).join(", "); - } - if (typeof reasons === "string") allReasons = reasons; - - let ticketName = ""; - - let ticketCount = (await client.prisma.$queryRaw<[{ count: bigint }]>`SELECT COUNT(*) as count FROM tickets`)[0].count; - - if (ticketType.ticketNameOption) { - ticketName = ticketType.ticketNameOption - .replace("USERNAME", interaction.user.username) - .replace("USERID", interaction.user.id) - .replace("TICKETCOUNT", ticketCount.toString() ?? "0"); - } else { - ticketName = client.config.ticketNameOption - .replace("USERNAME", interaction.user.username) - .replace("USERID", interaction.user.id) - .replace("TICKETCOUNT", ticketCount.toString() ?? "0"); - } - if (!interaction.guild) return console.error("Interaction createTicket was not executed in a guild"); - - const channel = await client.guilds.cache.get(client.config.guildId)?.channels.create({ - name: ticketName, - parent: ticketType.categoryId, - permissionOverwrites: [ - { - id: interaction.guild.roles.everyone, - deny: [PermissionFlagsBits.ViewChannel] - } - ] - }); - - if (!channel) return reject("Couldn't create the ticket channel."); - log( - { - LogType: "ticketCreate", - user: interaction.user, - reason: allReasons, - ticketChannelId: channel.id - }, - client - ); - - await channel.permissionOverwrites - .edit(interaction.user, { - SendMessages: true, - AddReactions: true, - ReadMessageHistory: true, - AttachFiles: true, - ViewChannel: true - }) - .catch((e) => console.log(e)); - - // Role Access Stuff - if (client.config.rolesWhoHaveAccessToTheTickets.length > 0 || (ticketType.staffRoles?.length ?? 0) > 0) { - for (const role of [...client.config.rolesWhoHaveAccessToTheTickets, ...(ticketType.staffRoles ?? [])]) - await channel.permissionOverwrites.edit(role, { - SendMessages: true, - AddReactions: true, - ReadMessageHistory: true, - AttachFiles: true, - ViewChannel: true - }); - } - - const footer = locale.getSubValue("embeds", "ticketOpened", "footer", "text").replace("ticket.pm", ""); - if (ticketType.color?.toString().trim() === "") ticketType.color = undefined; - const ticketOpenedEmbed = new EmbedBuilder({ - color: 0 - }) - .setColor(ticketType.color ?? client.config.mainColor) - .setTitle(locale.getSubValue("embeds", "ticketOpened", "title").replace("CATEGORYNAME", ticketType.name)) - .setDescription( - ticketType.customDescription - ? ticketType.customDescription - .replace("CATEGORYNAME", ticketType.name) - .replace("USERNAME", interaction.user.username) - .replace("USERID", interaction.user.id) - .replace("TICKETCOUNT", ticketCount.toString() ?? "0") - .replace("REASON1", reason[0]) - .replace("REASON2", reason[1]) - .replace("REASON3", reason[2]) - .replace("REASON4", reason[3]) - .replace("REASON5", reason[4]) - .replace("REASON6", reason[5]) - .replace("REASON7", reason[6]) - .replace("REASON8", reason[7]) - .replace("REASON9", reason[8]) - : locale - .getSubValue("embeds", "ticketOpened", "description") - .replace("CATEGORYNAME", ticketType.name) - .replace("USERNAME", interaction.user.username) - .replace("USERID", interaction.user.id) - .replace("TICKETCOUNT", ticketCount.toString() ?? "0") - .replace("REASON1", reason[0]) - .replace("REASON2", reason[1]) - .replace("REASON3", reason[2]) - .replace("REASON4", reason[3]) - .replace("REASON5", reason[4]) - .replace("REASON6", reason[5]) - .replace("REASON7", reason[6]) - .replace("REASON8", reason[7]) - .replace("REASON9", reason[8]) - ) - .setFooter({ - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - text: `ticket.pm ${footer.trim() !== "" ? `- ${footer}` : ""}`, // Please respect the LICENSE :D - // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - iconURL: locale.getNoErrorSubValue("embeds", "ticketOpened", "footer", "iconUrl") - }); - - // client.db is set here and incremented ticket count - ticketCount++; - - const row = new ActionRowBuilder(); - - if (client.config.closeOption?.closeButton) { - if (client.config.closeOption?.askReason) { - row.addComponents( - new ButtonBuilder() - .setCustomId("close_askReason") - .setLabel(locale.getSubValue("buttons", "close", "label")) - .setEmoji(locale.getSubValue("buttons", "close", "emoji")) - .setStyle(ButtonStyle.Danger) - ); - } else { - row.addComponents( - new ButtonBuilder() - .setCustomId("close") - .setLabel(locale.getSubValue("buttons", "close", "label")) - .setEmoji(locale.getSubValue("buttons", "close", "emoji")) - .setStyle(ButtonStyle.Danger) - ); - } - } - - if (client.config.claimOption?.claimButton) { - row.addComponents( - new ButtonBuilder() - .setCustomId("claim") - .setLabel(locale.getSubValue("buttons", "claim", "label")) - .setEmoji(locale.getSubValue("buttons", "claim", "emoji")) - .setStyle(ButtonStyle.Primary) - ); - } - - const body = { - embeds: [ticketOpenedEmbed], - content: `<@${interaction.user.id}> ${ - client.config.pingRoleWhenOpened ? client.config.roleToPingWhenOpenedId.map((x) => `<@&${x}>`).join(", ") : "" - }`, - components: [] as ActionRowBuilder[] - }; - - if (row.components.length > 0) body.components = [row]; - - channel - .send(body) - .then((msg) => { - client.prisma.tickets - .create({ - data: { - // @TODO: When releasing a new breaking version, store only the codeName for the category... - category: JSON.stringify(ticketType), - reason: allReasons, - creator: interaction.user.id, - createdat: Date.now(), - channelid: channel.id, - messageid: msg.id - } - }) - .then(); // Again why tf do I need .then()?!?!? - msg.pin().then(() => { - msg.channel.bulkDelete(1); - }); - interaction - .editReply({ - content: client.locales.getValue("ticketOpenedMessage").replace("TICKETCHANNEL", `<#${channel.id}>`), - components: [] - }) - .catch((e) => console.log(e)); - - resolve(true); - }) - .catch((e) => console.log(e)); - }); -}; diff --git a/src/utils/delete.ts b/src/utils/delete.ts deleted file mode 100644 index 3b9d8fd2..00000000 --- a/src/utils/delete.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -import { ButtonInteraction, ChannelType } from "discord.js"; -import { log } from "./logs"; -import {ExtendedClient} from "../structure"; - -export const deleteTicket = async (interaction: ButtonInteraction, client: ExtendedClient) => { - - if(!interaction.channel || interaction.channel.type !== ChannelType.GuildText) - return await interaction.reply({ - content: "This command can only be used in a ticket channel.", - ephemeral: true - }); - - const ticket = await client.prisma.tickets.findUnique({ - where: { - channelid: interaction.channel.id - } - }); - - if (!ticket) return await interaction.reply({ content: "Ticket not found", ephemeral: true }); - log( - { - LogType: "ticketDelete", - user: interaction.user, - ticketId: ticket.id, - ticketCreatedAt: ticket.createdat, - transcriptURL: ticket.transcript ?? undefined, - }, - client - ); - await interaction.deferUpdate(); - await interaction.channel.delete(); -}; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/utils/logs.ts b/src/utils/logs.ts deleted file mode 100644 index 153e10cf..00000000 --- a/src/utils/logs.ts +++ /dev/null @@ -1,167 +0,0 @@ -import Discord, { ChannelType, TextChannel, User } from "discord.js"; -import {ExtendedClient} from "../structure"; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ - -type log = { - LogType: "ticketCreate" - user: User - ticketChannelId?: string; - reason?: string; -} | { - LogType: "ticketClaim" | "ticketClose" - user: User - ticketChannelId?: string; - ticketId?: string | number; - reason?: string; - ticketCreatedAt: number | bigint; -} | { - LogType: "ticketDelete" - user: User - ticketChannelId?: string; - ticketId?: string | number; - reason?: string; - ticketCreatedAt: number | bigint; - transcriptURL?: string; - -} | { - LogType: "userAdded" | "userRemoved" - user: User; - target: { - id?: string - }; - ticketChannelId?: string; - reason?: string; - ticketId?: string | number; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -export const log = async(logs: log, client: ExtendedClient) => { - if (!client.config.logs) return; - if (!client.config.logsChannelId) return; - const channel = await client.channels - .fetch(client.config.logsChannelId) - .catch((e) => console.error("The channel to log events is not found!\n", e)); - if (!channel) return console.error("The channel to log events is not found!"); - if (!channel.isTextBased() || - channel.type === ChannelType.DM || - channel.type === ChannelType.PrivateThread || - channel.type === ChannelType.PublicThread) return console.error("Invalid Channel!"); - - const webhook = (await (channel as TextChannel).fetchWebhooks()).find((wh) => wh.token) ?? - await (channel as TextChannel).createWebhook({ name: "Ticket Bot Logs" }); - - if (logs.LogType === "ticketCreate") { - const embed = new Discord.EmbedBuilder() - .setColor("#3ba55c") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) - .setDescription(`${logs.user.tag} (<@${logs.user.id}>) Created a ticket (<#${logs.ticketChannelId}>) with the reason: \`${logs.reason}\``); - - webhook - .send({ - username: "Ticket Created", - avatarURL: "https://i.imgur.com/M38ZmjM.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - if (logs.LogType === "ticketClaim") { - const embed = new Discord.EmbedBuilder() - .setColor("#faa61a") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Claimed the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>) after ${client.msToHm( - new Date(Number(BigInt(Date.now()) - BigInt(logs.ticketCreatedAt))) - )} of creation` - ); - webhook - .send({ - username: "Ticket Claimed", - avatarURL: "https://i.imgur.com/qqEaUyR.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - - if (logs.LogType === "ticketClose") { - const embed = new Discord.EmbedBuilder() - .setColor("#ed4245") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Closed the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>) with the reason: \`${ - logs.reason - }\` after ${client.msToHm(Number(BigInt(Date.now()) - BigInt(logs.ticketCreatedAt)))} of creation` - ); - - webhook - .send({ - username: "Ticket Closed", - avatarURL: "https://i.imgur.com/5ShDA4g.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - - if (logs.LogType === "ticketDelete") { - const embed = new Discord.EmbedBuilder() - .setColor("#ed4245") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Deleted the ticket n°${logs.ticketId} after ${client.msToHm( - new Date(Number(BigInt(Date.now()) - BigInt(logs.ticketCreatedAt))) - )} of creation\n\nTranscript: ${logs.transcriptURL}` - ); - - webhook - .send({ - username: "Ticket Deleted", - avatarURL: "https://i.imgur.com/obTW2BS.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - - if (logs.LogType === "userAdded") { - const embed = new Discord.EmbedBuilder() - .setColor("#3ba55c") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Added <@${logs.target.id}> (${logs.target.id}) to the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>)` - ); - - webhook - .send({ - username: "User Added", - avatarURL: "https://i.imgur.com/G6QPFBV.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } - if (logs.LogType === "userRemoved") { - const embed = new Discord.EmbedBuilder() - .setColor("#ed4245") - .setAuthor({ name: logs.user.tag, iconURL: logs.user.displayAvatarURL() }) - .setDescription( - `${logs.user.tag} (<@${logs.user.id}>) Removed <@${logs.target.id}> (${logs.target.id}) from the ticket n°${logs.ticketId} (<#${logs.ticketChannelId}>)` - ); - webhook - .send({ - username: "User Removed", - avatarURL: "https://i.imgur.com/eFJ8xxC.png", - embeds: [embed], - }) - .catch((e) => console.log(e)); - } -}; - -/* -Copyright 2023 Sayrix (github.com/Sayrix) - -Licensed under the Creative Commons Attribution 4.0 International -please check https://creativecommons.org/licenses/by/4.0 for more informations. -*/ diff --git a/src/utils/translation.ts b/src/utils/translation.ts deleted file mode 100644 index 249e36c5..00000000 --- a/src/utils/translation.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* -Copyright © 2024 小兽兽/zhiyan114 (github.com/zhiyan114) -File is licensed respectively under the terms of the Creative Commons Attribution 4.0 International -or whichever license the project is using at the time https://github.com/Sayrix/Ticket-Bot/blob/main/LICENSE.md -*/ - -import path from "node:path"; -import fs from "fs-extra"; - -export class Translation { - private primaryData: {[k: string]: string | undefined}; - private backupData?: {[k: string]: string | undefined}; - - /** - * locale handler module - * @param optName The locale file name (w/o extension) - * @param dir The directory of the locale files - */ - constructor(optName: string, dir?: string) { - dir = dir ?? "./locale"; - const fullDir = path.join(dir, `${optName}.json`); - if(!fs.existsSync(fullDir)) - throw new TranslationError("Translation file not found, check your config to verify if the name is correct or not"); - - this.primaryData = JSON.parse(fs.readFileSync(fullDir, "utf8")); - if(optName !== "main") - this.backupData = JSON.parse(fs.readFileSync(path.join(dir, "main.json"), "utf8")); - } - - /** - * Get the translation value or backup value if it doesn't exist - * @param key The object key the translation should pull - * @returns the translation data or throw error if the translation data cannot be found at all - */ - getValue(key: string): string { - // Try return the data from the main translation file - const main = this.primaryData[key]; - if(main) return main; - - // Pull backup and throw error if it doesn't exist - const backup = this.backupData && this.backupData[key]; - if(!backup) - throw new TranslationError(`TRANSLATION: Key '${key}' failed to pull backup translation. This indicates this key data does not exist at all.`); - - // Return the backup translation - console.warn(`TRANSLATION: Key '${key}' is missing translation. If you can, please help fill in the translation and make PR for it.`); - return backup; - } - - /** - * Get the translation value that isn't on the top of the JSON object - * @param key All the keys leading to the value (or the classic dot access `"first.second"`) - * @returns the translation data or throw error if the translation data cannot be found at all - */ - // eslint-disable-next-line no-unused-vars - getSubValue(keys: string): string; - // eslint-disable-next-line no-unused-vars - getSubValue(...keys: string[]): string; - getSubValue(...keys: string[]): string { - // Convert the dot to array - if(keys.length === 1) - keys = keys[0].split("."); - - // Check the primary value first - let main: {[k: string]: string | undefined} | string | undefined = this.primaryData; - let bkup: {[k: string]: string | undefined} | string | undefined = this.backupData; - - for(const key of keys) { - if(typeof(main) === "object") - main = main[key]; - if(this.backupData && typeof(bkup) === "object") - bkup = bkup[key]; - } - - if(typeof(main) === "string" || typeof(main) === "number") return main; - if(typeof(bkup) !== "string" && typeof(bkup) !== "number") - throw new TranslationError(`TRANSLATION: Key '${keys.join(".")}' failed to pull backup translation. This indicates this key data does not exist at all.`); - console.warn(`TRANSLATION: Key '${keys.join(".")}' is missing translation. If you can, please help fill in the translation and make PR for it.`); - return bkup; - } - - /** - * Used for translation keys that can be empty - * @param keys All the keys leading to the value - * @returns the translation data or undefined if the translation data cannot be found - */ - getNoErrorSubValue(...keys: string[]): string | undefined { - try { - return this.getSubValue(...keys); - } catch { - return; - } - } - - /** - * Get the raw translation value (getSubValue but without string/number checks) - * @param key All the keys leading to the value (or the classic dot access `"first.second"`) - * @returns the translation data or throw error if the translation data cannot be found at all - */ - // eslint-disable-next-line no-unused-vars - getSubRawValue(keys: string): string | number | null | object; - // eslint-disable-next-line no-unused-vars - getSubRawValue(...keys: string[]): string | number | null | object; - getSubRawValue(...keys: string[]): string | number | null | object { - // Convert the dot to array - if(keys.length === 1) - keys = keys[0].split("."); - - // Check the primary value first - let main: {[k: string]: string | undefined} | string | undefined = this.primaryData; - let bkup: {[k: string]: string | undefined} | string | undefined = this.backupData; - - for(const key of keys) { - if(typeof(main) === "object") - main = main[key]; - if(this.backupData && typeof(bkup) === "object") - bkup = bkup[key]; - } - - if(main !== undefined) return main; - if(bkup === undefined) - throw new TranslationError(`TRANSLATION: Key '${keys.join(".")}' failed to pull backup translation. This indicates this key data does not exist at all.`); - console.warn(`TRANSLATION: Key '${keys.join(".")}' is missing translation. This is a raw value operation so please contact the dev before translating it.`); - return bkup; - } -} - -export class TranslationError { - name = "TranslationError"; - message: string; - constructor(msg: string) { - this.message = msg; - } -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 25b5b7ee..3f913b93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,35 @@ { - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src/", - "allowJs": true, - "module": "CommonJS", - "target": "ESNext", - "strict": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "esModuleInterop": true, - "skipLibCheck": false, - "sourceMap": true, - "inlineSources": true, - "sourceRoot": "/" - }, - "include": [ - "./src/**/*", - ] -} \ No newline at end of file + "compilerOptions": { + // Enable latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "baseUrl": ".", + "ignoreDeprecations": "6.0", + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + // Type definitions + "types": ["bun"], + + "paths": { + "@/*": ["./src/*"] + } + } +} From bfb0f29eb788cec2ff75da9f47827e04e98ce809 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:04:23 +0200 Subject: [PATCH 02/67] build(deps): add @discordjs packages * Add @discordjs/core, rest, ws * Add discord-api-types * Configure deploy and typecheck scripts * Update spell checker settings --- .vscode/settings.json | 2 +- bun.lock | 28 ++++++++++++++++++++++++++++ package.json | 6 ++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fab4c7e2..023de58e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["bunx", "libsql", "typesafe"] + "cSpell.words": ["bunx", "libsql", "replyable", "tsgo", "typesafe"] } diff --git a/bun.lock b/bun.lock index 586764ba..d2da85eb 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,11 @@ "workspaces": { "": { "dependencies": { + "@discordjs/core": "2.4.0", + "@discordjs/rest": "^2.6.1", + "@discordjs/ws": "^2.0.4", "@libsql/client": "^0.17.2", + "discord-api-types": "^0.38.44", "dotenv": "^17.4.1", "drizzle-orm": "^0.45.2", "typesafe-i18n": "^5.27.1", @@ -36,6 +40,16 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.10", "", { "os": "win32", "cpu": "x64" }, "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg=="], + "@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/core": ["@discordjs/core@2.4.0", "", { "dependencies": { "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^2.0.4", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.33" } }, "sha512-+y9kvW94Zc/3IVZVBktSnC2tK45LTonfmhZh+ExUUsBlfgorMY/A+11jAcCbtzz15NtNrtUOJiMA1MGGJkv0/A=="], + + "@discordjs/rest": ["@discordjs/rest@2.6.1", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "6.24.1" } }, "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg=="], + + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], + + "@discordjs/ws": ["@discordjs/ws@2.0.4", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@types/ws": "^8.5.12", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.32", "tslib": "^2.6.3", "ws": "^8.18.0" } }, "sha512-ARXnE+qi+D7Y4trd1bKA9uhiUxQvLbOKcdehDa6NLd7FiqmDvvk8N5RGk6Ho9gdT/Wap09dz/IuLv7hNpUzt6g=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], @@ -122,6 +136,10 @@ "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], + + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], @@ -144,6 +162,8 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260409.1", "", { "os": "win32", "cpu": "x64" }, "sha512-WRd+JpQipTsE15QgYr3w7J0f1NKvGcq2QEgmcq8hB0WZA1X2WhQopNu+MpPQ3tdDD42VjMhm8ZoB8HpuOoXK5w=="], + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], @@ -154,6 +174,8 @@ "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + "discord-api-types": ["discord-api-types@0.38.44", "", {}, "sha512-q91MgBzP/gRaCLIbQTaOrOhbD8uVIaPKxpgX2sfFB2nZ9nSiTYM9P3NFQ7cbO6NCxctI6ODttc67MI+YhIfILg=="], + "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], @@ -174,6 +196,8 @@ "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], + "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -188,12 +212,16 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "typesafe-i18n": ["typesafe-i18n@5.27.1", "", { "peerDependencies": { "typescript": ">=3.5.1" }, "bin": { "typesafe-i18n": "cli/typesafe-i18n.mjs" } }, "sha512-749uWo2ZXETT//kWjVYPm8QPYR8xLh8G0wLfoAyCAtAmysX67uCaAyLjAjAWojL6fuJpE5B6yIjwvO9orXzUPg=="], "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + "undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], diff --git a/package.json b/package.json index 0bc23a63..ce58b7a9 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,19 @@ "main": "src/index.ts", "scripts": { "start": "bun src/index.ts", + "deploy:commands": "bun src/deploy-commands.ts", + "typecheck": "tsgo --noEmit", "format": "biome format", "format:fix": "biome format --write", "lint": "biome lint", "i18n": "bunx typesafe-i18n --no-watch" }, "dependencies": { + "@discordjs/core": "2.4.0", + "@discordjs/rest": "^2.6.1", + "@discordjs/ws": "^2.0.4", "@libsql/client": "^0.17.2", + "discord-api-types": "^0.38.44", "dotenv": "^17.4.1", "drizzle-orm": "^0.45.2", "typesafe-i18n": "^5.27.1" From dc612c05d14fc36fdc0c91411f9838a56b2c4c5b Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:04:36 +0200 Subject: [PATCH 03/67] feat(core): setup custom handling framework * Implement file-based module discovery * Create event and command registry * Build central interaction router * Add custom-id encoding utilities * Establish core application types * Add response and logger helpers --- src/core/custom-id.ts | 25 ++++++ src/core/defineEvent.ts | 5 ++ src/core/defineFeature.ts | 5 ++ src/core/discovery.ts | 93 +++++++++++++++++++++ src/core/logger.ts | 30 +++++++ src/core/registry.ts | 67 +++++++++++++++ src/core/respond.ts | 36 ++++++++ src/core/router.ts | 167 ++++++++++++++++++++++++++++++++++++++ src/core/types.ts | 81 ++++++++++++++++++ 9 files changed, 509 insertions(+) create mode 100644 src/core/custom-id.ts create mode 100644 src/core/defineEvent.ts create mode 100644 src/core/defineFeature.ts create mode 100644 src/core/discovery.ts create mode 100644 src/core/logger.ts create mode 100644 src/core/registry.ts create mode 100644 src/core/respond.ts create mode 100644 src/core/router.ts create mode 100644 src/core/types.ts diff --git a/src/core/custom-id.ts b/src/core/custom-id.ts new file mode 100644 index 00000000..e09629a5 --- /dev/null +++ b/src/core/custom-id.ts @@ -0,0 +1,25 @@ +const CUSTOM_ID_SEPARATOR = ":"; + +export interface ParsedCustomId { + featureKey: string; + action: string; + state: string[]; +} + +export function createCustomId(featureKey: string, action: string, ...state: string[]) { + return [featureKey, action, ...state.map((part) => encodeURIComponent(part))].join(CUSTOM_ID_SEPARATOR); +} + +export function parseCustomId(customId: string): ParsedCustomId | null { + const [featureKey, action, ...rawState] = customId.split(CUSTOM_ID_SEPARATOR); + + if (!featureKey || !action) { + return null; + } + + return { + featureKey, + action, + state: rawState.map((part) => decodeURIComponent(part)) + }; +} diff --git a/src/core/defineEvent.ts b/src/core/defineEvent.ts new file mode 100644 index 00000000..cc27c71c --- /dev/null +++ b/src/core/defineEvent.ts @@ -0,0 +1,5 @@ +import type { EventModule } from "@/core/types"; + +export function defineEvent(event: EventModule) { + return event; +} diff --git a/src/core/defineFeature.ts b/src/core/defineFeature.ts new file mode 100644 index 00000000..2f8e2c08 --- /dev/null +++ b/src/core/defineFeature.ts @@ -0,0 +1,5 @@ +import type { FeatureModule } from "@/core/types"; + +export function defineFeature(feature: TFeature) { + return feature; +} diff --git a/src/core/discovery.ts b/src/core/discovery.ts new file mode 100644 index 00000000..22366caa --- /dev/null +++ b/src/core/discovery.ts @@ -0,0 +1,93 @@ +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import type { Logger } from "@/core/logger"; +import type { EventModule, FeatureModule } from "@/core/types"; + +const srcDirectory = fileURLToPath(new URL("..", import.meta.url)); +const featuresDirectory = join(srcDirectory, "features"); +const eventsDirectory = join(srcDirectory, "events"); + +function isFeatureModule(value: unknown): value is FeatureModule { + return typeof value === "object" && value !== null && "key" in value && typeof value.key === "string"; +} + +function isEventModule(value: unknown): value is EventModule { + return ( + typeof value === "object" && + value !== null && + "name" in value && + typeof value.name === "string" && + "execute" in value && + typeof value.execute === "function" + ); +} + +function isModuleFile(filePath: string) { + return filePath.endsWith(".ts") || filePath.endsWith(".js"); +} + +async function walkFiles(rootDirectory: string): Promise { + const entries = await readdir(rootDirectory, { withFileTypes: true }); + const files = await Promise.all( + entries.map(async (entry) => { + const absolutePath = join(rootDirectory, entry.name); + + if (entry.isDirectory()) { + return walkFiles(absolutePath); + } + + return [absolutePath]; + }) + ); + + return files.flat(); +} + +async function importModules( + directory: string, + matcher: (filePath: string) => boolean, + guard: (value: unknown) => value is TModule, + logger: Logger, + label: string +): Promise { + const filePaths = (await walkFiles(directory)).filter(matcher).sort(); + const loadedModules: TModule[] = []; + + for (const filePath of filePaths) { + const importedModule = await import(pathToFileURL(filePath).href); + const exportedValues = importedModule.default === undefined ? Object.values(importedModule) : [importedModule.default]; + + for (const exportedValue of exportedValues) { + if (!guard(exportedValue)) { + continue; + } + + loadedModules.push(exportedValue); + } + } + + logger.info(`Discovered ${loadedModules.length} ${label}.`); + + return loadedModules; +} + +export async function discoverFeatures(logger: Logger) { + return importModules( + featuresDirectory, + (filePath) => isModuleFile(filePath) && (filePath.endsWith("feature.ts") || filePath.endsWith("feature.js")), + isFeatureModule, + logger, + "features" + ); +} + +export async function discoverEvents(logger: Logger) { + return importModules( + eventsDirectory, + (filePath) => isModuleFile(filePath) && !filePath.endsWith("index.ts") && !filePath.endsWith("index.js"), + isEventModule, + logger, + "events" + ); +} diff --git a/src/core/logger.ts b/src/core/logger.ts new file mode 100644 index 00000000..71e469f3 --- /dev/null +++ b/src/core/logger.ts @@ -0,0 +1,30 @@ +export interface Logger { + info(message: string, ...meta: unknown[]): void; + warn(message: string, ...meta: unknown[]): void; + error(message: string, ...meta: unknown[]): void; +} + +export function createLogger(scope: string): Logger { + const write = (level: string, message: string, meta: unknown[]) => { + const prefix = `[${scope}] ${level}`; + + if (meta.length === 0) { + console.log(prefix, message); + return; + } + + console.log(prefix, message, ...meta); + }; + + return { + info(message, ...meta) { + write("INFO", message, meta); + }, + warn(message, ...meta) { + write("WARN", message, meta); + }, + error(message, ...meta) { + write("ERROR", message, meta); + } + }; +} diff --git a/src/core/registry.ts b/src/core/registry.ts new file mode 100644 index 00000000..c3777a20 --- /dev/null +++ b/src/core/registry.ts @@ -0,0 +1,67 @@ +import type { Logger } from "@/core/logger"; +import type { BotApp, EventModule, FeatureModule, HandlerRegistry, RegisteredCommand } from "@/core/types"; + +interface CreateHandlerRegistryInput { + events: EventModule[]; + features: FeatureModule[]; + logger: Logger; +} + +export function createHandlerRegistry({ events, features, logger }: CreateHandlerRegistryInput): HandlerRegistry { + const featureMap = new Map(); + const commandMap = new Map(); + + for (const feature of features) { + if (featureMap.has(feature.key)) { + throw new Error(`Duplicate feature key "${feature.key}" detected.`); + } + + featureMap.set(feature.key, feature); + + for (const command of feature.commands ?? []) { + if (commandMap.has(command.data.name)) { + throw new Error(`Duplicate slash command "${command.data.name}" detected.`); + } + + commandMap.set(command.data.name, { + command, + feature + }); + } + } + + logger.info(`Registered ${featureMap.size} feature modules.`); + + return { + events, + features: featureMap, + commands: commandMap, + applicationCommands: [...commandMap.values()].map(({ command }) => command.data) + }; +} + +export function registerEvents(app: BotApp) { + const eventClient = app.client as unknown as { + on(name: never, listener: (...args: unknown[]) => void): void; + once(name: never, listener: (...args: unknown[]) => void): void; + }; + + for (const event of app.registry.events) { + const listener = (...args: unknown[]) => { + void (async () => { + try { + await event.execute(app, ...args); + } catch (error) { + app.logger.error(`Event "${event.name}" failed.`, error); + } + })(); + }; + + if (event.once) { + eventClient.once(event.name as never, listener); + continue; + } + + eventClient.on(event.name as never, listener); + } +} diff --git a/src/core/respond.ts b/src/core/respond.ts new file mode 100644 index 00000000..d343b086 --- /dev/null +++ b/src/core/respond.ts @@ -0,0 +1,36 @@ +import type { + APIApplicationCommandAutocompleteInteraction, + APIChatInputApplicationCommandInteraction, + APIMessageComponentInteraction, + APIModalSubmitInteraction +} from "@discordjs/core"; +import { MessageFlags } from "@discordjs/core"; +import type { BotApp } from "@/core/types"; + +type ReplyableInteraction = + | APIChatInputApplicationCommandInteraction + | APIMessageComponentInteraction + | APIModalSubmitInteraction; + +export async function reply(app: BotApp, interaction: ReplyableInteraction, body: any) { + return app.client.api.interactions.reply(interaction.id, interaction.token, body); +} + +export async function updateMessage(app: BotApp, interaction: APIMessageComponentInteraction, body: any) { + return app.client.api.interactions.updateMessage(interaction.id, interaction.token, body); +} + +export async function showModal(app: BotApp, interaction: APIMessageComponentInteraction, body: any) { + return app.client.api.interactions.createModal(interaction.id, interaction.token, body); +} + +export async function replyWithAutocomplete(app: BotApp, interaction: APIApplicationCommandAutocompleteInteraction, body: any) { + return app.client.api.interactions.createAutocompleteResponse(interaction.id, interaction.token, body); +} + +export async function replyWithError(app: BotApp, interaction: ReplyableInteraction) { + return reply(app, interaction, { + content: "An unexpected error occurred while handling this interaction.", + flags: MessageFlags.Ephemeral + }).catch(() => undefined); +} diff --git a/src/core/router.ts b/src/core/router.ts new file mode 100644 index 00000000..277b7391 --- /dev/null +++ b/src/core/router.ts @@ -0,0 +1,167 @@ +import type { + APIApplicationCommandAutocompleteInteraction, + APIApplicationCommandInteraction, + APIChatInputApplicationCommandInteraction, + APIMessageComponentInteraction, + APIModalSubmitInteraction +} from "@discordjs/core"; +import { ApplicationCommandType, ComponentType, InteractionType } from "@discordjs/core"; +import { parseCustomId } from "@/core/custom-id"; +import { replyWithError } from "@/core/respond"; +import type { BotApp, ComponentExecutionContext, FeatureContext, RoutedInteraction } from "@/core/types"; + +function isChatInputCommand( + interaction: APIApplicationCommandInteraction +): interaction is APIChatInputApplicationCommandInteraction { + return interaction.data.type === ApplicationCommandType.ChatInput; +} + +export class InteractionRouter { + public constructor(private readonly app: BotApp) {} + + public async handleInteraction(interaction: RoutedInteraction) { + try { + switch (interaction.type) { + case InteractionType.ApplicationCommand: + await this.handleApplicationCommand(interaction as APIApplicationCommandInteraction); + return; + case InteractionType.ApplicationCommandAutocomplete: + await this.handleAutocomplete(interaction as APIApplicationCommandAutocompleteInteraction); + return; + case InteractionType.MessageComponent: + await this.handleMessageComponent(interaction as APIMessageComponentInteraction); + return; + case InteractionType.ModalSubmit: + await this.handleModalSubmit(interaction as APIModalSubmitInteraction); + return; + default: + return; + } + } catch (error) { + this.app.logger.error("Failed to route interaction", error); + + if (interaction.type !== InteractionType.ApplicationCommandAutocomplete) { + await replyWithError( + this.app, + interaction as APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction | APIModalSubmitInteraction + ); + } + } + } + + private async handleApplicationCommand(interaction: APIApplicationCommandInteraction) { + if (!isChatInputCommand(interaction)) { + return; + } + + await this.handleSlashCommand(interaction); + } + + private async handleSlashCommand(interaction: APIChatInputApplicationCommandInteraction) { + const registeredCommand = this.app.registry.commands.get(interaction.data.name); + + if (!registeredCommand) { + this.app.logger.warn(`No slash command registered for "${interaction.data.name}".`); + return; + } + + const context: FeatureContext = { + app: this.app, + feature: registeredCommand.feature + }; + + await registeredCommand.command.execute(context, interaction); + } + + private async handleAutocomplete(interaction: APIApplicationCommandAutocompleteInteraction) { + const registeredCommand = this.app.registry.commands.get(interaction.data.name); + + if (!registeredCommand?.command.autocomplete) { + return; + } + + const context: FeatureContext = { + app: this.app, + feature: registeredCommand.feature + }; + + await registeredCommand.command.autocomplete(context, interaction); + } + + private async handleMessageComponent(interaction: APIMessageComponentInteraction) { + const route = parseCustomId(interaction.data.custom_id); + + if (!route) { + this.app.logger.warn(`Invalid custom id "${interaction.data.custom_id}".`); + return; + } + + const feature = this.app.registry.features.get(route.featureKey); + + if (!feature) { + this.app.logger.warn(`No feature registered for "${route.featureKey}".`); + return; + } + + const context: ComponentExecutionContext = { + app: this.app, + feature, + route + }; + + if (interaction.data.component_type === ComponentType.Button) { + const handler = feature.buttons?.[route.action]; + + if (!handler) { + this.app.logger.warn(`No button handler "${route.featureKey}:${route.action}".`); + return; + } + + await handler(context, interaction); + return; + } + + if (interaction.data.component_type === ComponentType.StringSelect) { + const handler = feature.stringSelects?.[route.action]; + + if (!handler) { + this.app.logger.warn(`No string select handler "${route.featureKey}:${route.action}".`); + return; + } + + await handler(context, interaction); + } + } + + private async handleModalSubmit(interaction: APIModalSubmitInteraction) { + const route = parseCustomId(interaction.data.custom_id); + + if (!route) { + this.app.logger.warn(`Invalid modal custom id "${interaction.data.custom_id}".`); + return; + } + + const feature = this.app.registry.features.get(route.featureKey); + + if (!feature) { + this.app.logger.warn(`No feature registered for modal "${route.featureKey}".`); + return; + } + + const context: ComponentExecutionContext = { + app: this.app, + feature, + route + }; + + const handler = feature.modals?.[route.action]; + + if (!handler) { + this.app.logger.warn(`No modal handler "${route.featureKey}:${route.action}".`); + await replyWithError(this.app, interaction); + return; + } + + await handler(context, interaction); + } +} diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 00000000..d3ec0e6c --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,81 @@ +import type { + APIApplicationCommandAutocompleteInteraction, + APIApplicationCommandInteraction, + APIChatInputApplicationCommandInteraction, + APIMessageComponentInteraction, + APIModalSubmitInteraction, + Client +} from "@discordjs/core"; +import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord-api-types/v10"; +import type { drizzle } from "drizzle-orm/libsql"; +import type { ParsedCustomId } from "@/core/custom-id"; +import type { Logger } from "@/core/logger"; + +export type RoutedInteraction = + | APIApplicationCommandAutocompleteInteraction + | APIApplicationCommandInteraction + | APIMessageComponentInteraction + | APIModalSubmitInteraction; + +export interface SlashCommandDefinition { + data: RESTPostAPIChatInputApplicationCommandsJSONBody; + execute(context: FeatureContext, interaction: APIChatInputApplicationCommandInteraction): Promise; + autocomplete?(context: FeatureContext, interaction: APIApplicationCommandAutocompleteInteraction): Promise; +} + +export interface FeatureModule { + key: string; + commands?: SlashCommandDefinition[]; + buttons?: Record; + stringSelects?: Record; + modals?: Record; +} + +export interface RegisteredCommand { + command: SlashCommandDefinition; + feature: FeatureModule; +} + +export interface EventModule { + name: string; + once?: boolean; + execute(app: BotApp, ...args: TArgs): Promise; +} + +export interface HandlerRegistry { + events: EventModule[]; + features: Map; + commands: Map; + applicationCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[]; +} + +export interface InteractionRouterContract { + handleInteraction(interaction: RoutedInteraction): Promise; +} + +export interface BotApp { + client: Client; + db: ReturnType; + applicationId: string; + logger: Logger; + registry: HandlerRegistry; + router: InteractionRouterContract; +} + +export interface FeatureContext { + app: BotApp; + feature: FeatureModule; +} + +export interface ComponentExecutionContext extends FeatureContext { + route: ParsedCustomId; +} + +export type ButtonHandler = (context: ComponentExecutionContext, interaction: APIMessageComponentInteraction) => Promise; + +export type StringSelectHandler = ( + context: ComponentExecutionContext, + interaction: APIMessageComponentInteraction +) => Promise; + +export type ModalHandler = (context: ComponentExecutionContext, interaction: APIModalSubmitInteraction) => Promise; From 645025793c2c6d8915cdca7f5624185f91dddf79 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:04:37 +0200 Subject: [PATCH 04/67] refactor(bot): build app-based lifecycle * Extract start logic to createBotApp * Connect gateway and interaction router * Handle graceful process shutdowns * Add script to deploy commands * Register core interaction and ready events --- src/app.ts | 56 +++++++++++++++++++++++++++++++++ src/deploy-commands.ts | 33 +++++++++++++++++++ src/events/interactionCreate.ts | 16 ++++++++++ src/events/ready.ts | 12 +++++++ src/index.ts | 25 +++++++++++++-- 5 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 src/app.ts create mode 100644 src/deploy-commands.ts create mode 100644 src/events/interactionCreate.ts create mode 100644 src/events/ready.ts diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 00000000..4461242f --- /dev/null +++ b/src/app.ts @@ -0,0 +1,56 @@ +import { Client, GatewayIntentBits } from "@discordjs/core"; +import { REST } from "@discordjs/rest"; +import { WebSocketManager } from "@discordjs/ws"; +import { drizzle } from "drizzle-orm/libsql"; +import { discoverEvents, discoverFeatures } from "@/core/discovery"; +import { createLogger } from "@/core/logger"; +import { createHandlerRegistry, registerEvents } from "@/core/registry"; +import { InteractionRouter } from "@/core/router"; +import type { BotApp } from "@/core/types"; +import botConfig from "../config/config.ts"; + +export async function createBotApp() { + const logger = createLogger("bot"); + const db = drizzle(process.env.DB_FILE_NAME); + + const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); + const gateway = new WebSocketManager({ + token: process.env.DISCORD_TOKEN, + intents: GatewayIntentBits.Guilds, + rest + }); + + const client = new Client({ rest, gateway: gateway as never }); + const [events, features] = await Promise.all([discoverEvents(logger), discoverFeatures(logger)]); + const registry = createHandlerRegistry({ features, events, logger }); + + const app = {} as BotApp; + app.client = client; + app.db = db; + app.logger = logger; + app.applicationId = botConfig.clientId; + app.registry = registry; + + app.router = new InteractionRouter(app); + + registerEvents(app); + + rest.on("rateLimited", (info: unknown) => { + logger.warn("Discord REST rate limit", info); + }); + + return { + app, + async start() { + logger.info(`Loaded ${registry.features.size} features and ${registry.commands.size} slash commands.`); + await gateway.connect(); + }, + async stop() { + try { + await gateway.destroy(); + } catch (error) { + logger.warn("Failed to destroy gateway cleanly", error); + } + } + }; +} diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts new file mode 100644 index 00000000..eff752f1 --- /dev/null +++ b/src/deploy-commands.ts @@ -0,0 +1,33 @@ +import { API } from "@discordjs/core"; +import { REST } from "@discordjs/rest"; +import { config } from "dotenv"; +import { discoverEvents, discoverFeatures } from "@/core/discovery"; +import { createLogger } from "@/core/logger"; +import { createHandlerRegistry } from "@/core/registry"; +import botConfig from "../config/config.ts"; + +config({ path: "./config/.env" }); +const logger = createLogger("deploy"); +const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); +const api = new API(rest); + +async function deployCommands() { + const [events, features] = await Promise.all([discoverEvents(logger), discoverFeatures(logger)]); + const registry = createHandlerRegistry({ features, events, logger }); + + if (botConfig.guildId) { + await api.applicationCommands.bulkOverwriteGuildCommands(botConfig.clientId, botConfig.guildId, registry.applicationCommands); + + logger.info(`Deployed ${registry.applicationCommands.length} guild commands to ${botConfig.guildId}.`); + return; + } + + await api.applicationCommands.bulkOverwriteGlobalCommands(botConfig.clientId, registry.applicationCommands); + + logger.info(`Deployed ${registry.applicationCommands.length} global commands.`); +} + +deployCommands().catch((error) => { + logger.error("Failed to deploy commands", error); + process.exit(1); +}); diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts new file mode 100644 index 00000000..84bd403a --- /dev/null +++ b/src/events/interactionCreate.ts @@ -0,0 +1,16 @@ +import type { GatewayInteractionCreateDispatchData, ToEventProps } from "@discordjs/core"; +import { GatewayDispatchEvents, InteractionType } from "@discordjs/core"; +import { defineEvent } from "@/core/defineEvent"; + +const interactionCreateEvent = defineEvent<[ToEventProps]>({ + name: GatewayDispatchEvents.InteractionCreate, + async execute(app, event) { + if (event.data.type === InteractionType.Ping) { + return; + } + + await app.router.handleInteraction(event.data); + } +}); + +export default interactionCreateEvent; diff --git a/src/events/ready.ts b/src/events/ready.ts new file mode 100644 index 00000000..b1ce24f1 --- /dev/null +++ b/src/events/ready.ts @@ -0,0 +1,12 @@ +import { GatewayDispatchEvents, type GatewayReadyDispatchData, type ToEventProps } from "@discordjs/core"; +import { defineEvent } from "@/core/defineEvent"; + +const readyEvent = defineEvent<[ToEventProps]>({ + name: GatewayDispatchEvents.Ready, + once: true, + async execute(app, event) { + app.logger.info(`Connected as ${event.data.user.username}.`); + } +}); + +export default readyEvent; diff --git a/src/index.ts b/src/index.ts index 375039e5..926aa0b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,23 @@ -import "dotenv/config"; -import { drizzle } from "drizzle-orm/libsql"; +import { config } from "dotenv"; +import { createBotApp } from "@/app"; -const db = drizzle(process.env.DB_FILE_NAME); +config({ path: "./config/.env" }); + +async function main() { + const { start, stop } = await createBotApp(); + + process.on("SIGINT", () => { + void stop().finally(() => process.exit(0)); + }); + + process.on("SIGTERM", () => { + void stop().finally(() => process.exit(0)); + }); + + await start(); +} + +main().catch(async (error) => { + console.error("[boot] Failed to start bot", error); + process.exit(1); +}); From 8ba7384d4c26f7616738d2033ad76f917fe751bc Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:51:44 +0200 Subject: [PATCH 05/67] feat(license): add LICENSE and NOTICE files with AGPL-3.0-only details --- LICENSE.md | 672 +++++++++++++++++++++++++++++++++++++++++++++++++++ NOTICE | 12 + package.json | 1 + 3 files changed, 685 insertions(+) create mode 100644 LICENSE.md create mode 100644 NOTICE diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..22171517 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,672 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. + + +## Additional Term under GNU AGPL v3, Section 7(b) + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..0af12e0c --- /dev/null +++ b/NOTICE @@ -0,0 +1,12 @@ +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. diff --git a/package.json b/package.json index ce58b7a9..cc1b8e29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "ticket-bot", "version": "4.0.0", + "license": "AGPL-3.0-only", "description": "Open-source Discord ticket bot.", "main": "src/index.ts", "scripts": { From 81ee66ec6453926160e945f7024420812466c103 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:05:58 +0200 Subject: [PATCH 06/67] build: update dependencies and typescript config * Add @ticketpm and libsql client dependencies * Configure tsgo, typesafe-i18n, biome formating scripts * Define typings for discordjs rest client * Add workspace words to cSpell dictionary --- .vscode/settings.json | 2 +- bun.lock | 8 ++++++++ package.json | 11 +++++++---- src/types/discordjs-rest.d.ts | 7 +++++++ tsconfig.json | 1 - 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 src/types/discordjs-rest.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 023de58e..39eb4509 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["bunx", "libsql", "replyable", "tsgo", "typesafe"] + "cSpell.words": ["bunx", "cleardm", "libsql", "replyable", "ticketpm", "tsgo", "typesafe"] } diff --git a/bun.lock b/bun.lock index d2da85eb..b6a241f0 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,8 @@ "@discordjs/rest": "^2.6.1", "@discordjs/ws": "^2.0.4", "@libsql/client": "^0.17.2", + "@ticketpm/core": "^0.0.6", + "@ticketpm/discord-api": "^0.0.6", "discord-api-types": "^0.38.44", "dotenv": "^17.4.1", "drizzle-orm": "^0.45.2", @@ -140,6 +142,10 @@ "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + "@ticketpm/core": ["@ticketpm/core@0.0.6", "", {}, "sha512-wjzubX8a3tNePJeAGAlIr7vTBVNzlEOmD4CBC+tqwQhU1PiqQUJWeLKhkmh87a2NAd6AIRJXifUfzYRiHiZuIw=="], + + "@ticketpm/discord-api": ["@ticketpm/discord-api@0.0.6", "", { "dependencies": { "@ticketpm/core": "0.0.6", "discord-api-types": "^0.37.119" } }, "sha512-1pKjIvy1Xzn4kLzfgfxrrSrpiLoa6YQpG5CEzaGQfWjuov7CK2OxFfcGZzKtMgBX2hNVsrQYycBqzzxJ4SoDzA=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], @@ -234,6 +240,8 @@ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@ticketpm/discord-api/discord-api-types": ["discord-api-types@0.37.120", "", {}, "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw=="], + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], diff --git a/package.json b/package.json index cc1b8e29..8659f16c 100644 --- a/package.json +++ b/package.json @@ -5,19 +5,22 @@ "description": "Open-source Discord ticket bot.", "main": "src/index.ts", "scripts": { - "start": "bun src/index.ts", - "deploy:commands": "bun src/deploy-commands.ts", "typecheck": "tsgo --noEmit", + "i18n": "bunx typesafe-i18n --no-watch", + "lint": "biome lint", "format": "biome format", "format:fix": "biome format --write", - "lint": "biome lint", - "i18n": "bunx typesafe-i18n --no-watch" + "drizzle:push": "bunx drizzle-kit push", + "deploy:commands": "bun src/deploy-commands.ts", + "start": "bun src/index.ts" }, "dependencies": { "@discordjs/core": "2.4.0", "@discordjs/rest": "^2.6.1", "@discordjs/ws": "^2.0.4", "@libsql/client": "^0.17.2", + "@ticketpm/core": "^0.0.6", + "@ticketpm/discord-api": "^0.0.6", "discord-api-types": "^0.38.44", "dotenv": "^17.4.1", "drizzle-orm": "^0.45.2", diff --git a/src/types/discordjs-rest.d.ts b/src/types/discordjs-rest.d.ts new file mode 100644 index 00000000..7203305d --- /dev/null +++ b/src/types/discordjs-rest.d.ts @@ -0,0 +1,7 @@ +declare module "@discordjs/rest" { + export class REST { + public constructor(options?: { version?: string }); + public setToken(token: string | undefined): REST; + public on(event: string, listener: (payload: unknown) => void): void; + } +} diff --git a/tsconfig.json b/tsconfig.json index 3f913b93..8cf2c935 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,6 @@ "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, - "baseUrl": ".", "ignoreDeprecations": "6.0", // Best practices From e232c1caaa4a4611ea5382c85fcc44b17670c536 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:06:11 +0200 Subject: [PATCH 07/67] refactor(core): enhance command discovery and state schemas * Enable standalone slash command discovery and parsing * Support deferred and editable interaction responses * Introduce versioned config schemas for ticket panels * Add database bootstrapping logic for dynamic schema migrations * Inject versioned config and app instances into execution context --- src/app.ts | 11 +++-- src/config/index.ts | 92 +++++++++++++++++++++++++++++++++++---- src/core/defineCommand.ts | 5 +++ src/core/discovery.ts | 26 ++++++++++- src/core/registry.ts | 24 +++++----- src/core/respond.ts | 16 ++++++- src/core/router.ts | 24 +++++----- src/core/types.ts | 20 ++++----- src/db/schema.ts | 10 +++++ src/deploy-commands.ts | 10 +++-- 10 files changed, 185 insertions(+), 53 deletions(-) create mode 100644 src/core/defineCommand.ts diff --git a/src/app.ts b/src/app.ts index 4461242f..99fe2fcd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,7 +2,7 @@ import { Client, GatewayIntentBits } from "@discordjs/core"; import { REST } from "@discordjs/rest"; import { WebSocketManager } from "@discordjs/ws"; import { drizzle } from "drizzle-orm/libsql"; -import { discoverEvents, discoverFeatures } from "@/core/discovery"; +import { discoverCommands, discoverEvents, discoverFeatures } from "@/core/discovery"; import { createLogger } from "@/core/logger"; import { createHandlerRegistry, registerEvents } from "@/core/registry"; import { InteractionRouter } from "@/core/router"; @@ -21,12 +21,17 @@ export async function createBotApp() { }); const client = new Client({ rest, gateway: gateway as never }); - const [events, features] = await Promise.all([discoverEvents(logger), discoverFeatures(logger)]); - const registry = createHandlerRegistry({ features, events, logger }); + const [commands, events, features] = await Promise.all([ + discoverCommands(logger), + discoverEvents(logger), + discoverFeatures(logger) + ]); + const registry = createHandlerRegistry({ commands, features, events, logger }); const app = {} as BotApp; app.client = client; app.db = db; + app.config = botConfig; app.logger = logger; app.applicationId = botConfig.clientId; app.registry = registry; diff --git a/src/config/index.ts b/src/config/index.ts index 9b5e97da..adf5c53a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -5,21 +5,93 @@ interface ConfigV0_0_1 { guildId: string; /** The lang of the bot */ lang: "en"; - - /** The ticket types / categories the users can choose */ + tickets: { + channelNameTemplate: string; + maxOpenPerUser: number; + staffRoleIds: string[]; + blockedRoleIds: string[]; + mentionRoleIds: string[]; + defaultWelcomeMessage?: string; + defaultWelcomeContent?: string; + claims: { + enabled: boolean; + mode: "soft" | "strict" | "display-only"; + showButtons: boolean; + allowUnclaim: boolean; + nameWhenClaimed?: string; + categoryWhenClaimed?: string; + takeoverMode: "disabled" | "staff" | "roles"; + takeoverRoleIds?: string[]; + }; + close: { + staffOnly: boolean; + dmUserOnClose: boolean; + askForReason: boolean; + showCloseButton: boolean; + deleteChannelOnClose: boolean; + createTranscript: boolean; + closeTicketCategoryId?: string; + dmMessage?: string; + channelMessage?: string; + }; + }; ticketTypes: Record< string, { - /** The name of the ticket type */ name: string; - /** The description displayed in the select menu */ description?: string; - /** The emoji displayed in the select menu. Can be unicode or custom emoji ID. */ emoji?: string; - /** The ID of the category where the ticket channels will be created. */ categoryId: string; - /** The name of the ticket channel */ - ticketName: string; + channelNameTemplate?: string; + message?: string; + welcomeContent?: string; + blockedRoleIds?: string[]; + staffRoleIds?: string[]; + openForm?: { + title: string; + questions: Array<{ + key: string; + label: string; + placeholder?: string; + style?: "short" | "paragraph"; + required?: boolean; + minLength?: number; + maxLength?: number; + }>; + }; + } + >; + panels: Record< + string, + { + channelId: string; + message: string; + content?: string; + opener: + | { + type: "inline-select"; + ticketTypes: string[]; + placeholder?: string; + } + | { + type: "button-select"; + ticketTypes: string[]; + label: string; + emoji?: string; + style?: "primary" | "secondary" | "success" | "danger"; + placeholder?: string; + disabled?: boolean; + } + | { + type: "buttons"; + buttons: Array<{ + ticketType: string; + label?: string; + emoji?: string; + style?: "primary" | "secondary" | "success" | "danger"; + disabled?: boolean; + }>; + }; } >; } @@ -32,10 +104,12 @@ type ConfigVersion = keyof ConfigVersions; type ConfigOf = ConfigVersions[V]; -type VersionedConfig = { +export type VersionedConfig = { version: V; } & ConfigOf; +export type AnyVersionedConfig = VersionedConfig; + export function defineConfig(version: V, config: ConfigOf): VersionedConfig { return { version, diff --git a/src/core/defineCommand.ts b/src/core/defineCommand.ts new file mode 100644 index 00000000..0ee50e8c --- /dev/null +++ b/src/core/defineCommand.ts @@ -0,0 +1,5 @@ +import type { CommandModule } from "@/core/types"; + +export function defineCommand(command: TCommand) { + return command; +} diff --git a/src/core/discovery.ts b/src/core/discovery.ts index 22366caa..cfb53538 100644 --- a/src/core/discovery.ts +++ b/src/core/discovery.ts @@ -2,12 +2,26 @@ import { readdir } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import type { Logger } from "@/core/logger"; -import type { EventModule, FeatureModule } from "@/core/types"; +import type { CommandModule, EventModule, FeatureModule } from "@/core/types"; const srcDirectory = fileURLToPath(new URL("..", import.meta.url)); const featuresDirectory = join(srcDirectory, "features"); const eventsDirectory = join(srcDirectory, "events"); +function isCommandModule(value: unknown): value is CommandModule { + return ( + typeof value === "object" && + value !== null && + "data" in value && + typeof value.data === "object" && + value.data !== null && + "name" in value.data && + typeof value.data.name === "string" && + "execute" in value && + typeof value.execute === "function" + ); +} + function isFeatureModule(value: unknown): value is FeatureModule { return typeof value === "object" && value !== null && "key" in value && typeof value.key === "string"; } @@ -82,6 +96,16 @@ export async function discoverFeatures(logger: Logger) { ); } +export async function discoverCommands(logger: Logger) { + return importModules( + featuresDirectory, + (filePath) => isModuleFile(filePath) && (filePath.endsWith("command.ts") || filePath.endsWith("command.js")), + isCommandModule, + logger, + "commands" + ); +} + export async function discoverEvents(logger: Logger) { return importModules( eventsDirectory, diff --git a/src/core/registry.ts b/src/core/registry.ts index c3777a20..6e86a99e 100644 --- a/src/core/registry.ts +++ b/src/core/registry.ts @@ -1,15 +1,16 @@ import type { Logger } from "@/core/logger"; -import type { BotApp, EventModule, FeatureModule, HandlerRegistry, RegisteredCommand } from "@/core/types"; +import type { BotApp, CommandModule, EventModule, FeatureModule, HandlerRegistry } from "@/core/types"; interface CreateHandlerRegistryInput { + commands: CommandModule[]; events: EventModule[]; features: FeatureModule[]; logger: Logger; } -export function createHandlerRegistry({ events, features, logger }: CreateHandlerRegistryInput): HandlerRegistry { +export function createHandlerRegistry({ commands, events, features, logger }: CreateHandlerRegistryInput): HandlerRegistry { const featureMap = new Map(); - const commandMap = new Map(); + const commandMap = new Map(); for (const feature of features) { if (featureMap.has(feature.key)) { @@ -17,17 +18,14 @@ export function createHandlerRegistry({ events, features, logger }: CreateHandle } featureMap.set(feature.key, feature); + } - for (const command of feature.commands ?? []) { - if (commandMap.has(command.data.name)) { - throw new Error(`Duplicate slash command "${command.data.name}" detected.`); - } - - commandMap.set(command.data.name, { - command, - feature - }); + for (const command of commands) { + if (commandMap.has(command.data.name)) { + throw new Error(`Duplicate slash command "${command.data.name}" detected.`); } + + commandMap.set(command.data.name, command); } logger.info(`Registered ${featureMap.size} feature modules.`); @@ -36,7 +34,7 @@ export function createHandlerRegistry({ events, features, logger }: CreateHandle events, features: featureMap, commands: commandMap, - applicationCommands: [...commandMap.values()].map(({ command }) => command.data) + applicationCommands: [...commandMap.values()].map((command) => command.data) }; } diff --git a/src/core/respond.ts b/src/core/respond.ts index d343b086..0ec1ecdd 100644 --- a/src/core/respond.ts +++ b/src/core/respond.ts @@ -12,15 +12,29 @@ type ReplyableInteraction = | APIMessageComponentInteraction | APIModalSubmitInteraction; +type ModalCapableInteraction = APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction; + export async function reply(app: BotApp, interaction: ReplyableInteraction, body: any) { return app.client.api.interactions.reply(interaction.id, interaction.token, body); } +export async function deferReply(app: BotApp, interaction: ReplyableInteraction, body?: any) { + return app.client.api.interactions.defer(interaction.id, interaction.token, body); +} + +export async function editReply(app: BotApp, interaction: ReplyableInteraction, body: any) { + return app.client.api.interactions.editReply(app.applicationId, interaction.token, body); +} + +export async function followUp(app: BotApp, interaction: ReplyableInteraction, body: any) { + return app.client.api.interactions.followUp(app.applicationId, interaction.token, body); +} + export async function updateMessage(app: BotApp, interaction: APIMessageComponentInteraction, body: any) { return app.client.api.interactions.updateMessage(interaction.id, interaction.token, body); } -export async function showModal(app: BotApp, interaction: APIMessageComponentInteraction, body: any) { +export async function showModal(app: BotApp, interaction: ModalCapableInteraction, body: any) { return app.client.api.interactions.createModal(interaction.id, interaction.token, body); } diff --git a/src/core/router.ts b/src/core/router.ts index 277b7391..50a241bc 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -8,7 +8,7 @@ import type { import { ApplicationCommandType, ComponentType, InteractionType } from "@discordjs/core"; import { parseCustomId } from "@/core/custom-id"; import { replyWithError } from "@/core/respond"; -import type { BotApp, ComponentExecutionContext, FeatureContext, RoutedInteraction } from "@/core/types"; +import type { BotApp, CommandExecutionContext, ComponentExecutionContext, RoutedInteraction } from "@/core/types"; function isChatInputCommand( interaction: APIApplicationCommandInteraction @@ -58,34 +58,32 @@ export class InteractionRouter { } private async handleSlashCommand(interaction: APIChatInputApplicationCommandInteraction) { - const registeredCommand = this.app.registry.commands.get(interaction.data.name); + const command = this.app.registry.commands.get(interaction.data.name); - if (!registeredCommand) { + if (!command) { this.app.logger.warn(`No slash command registered for "${interaction.data.name}".`); return; } - const context: FeatureContext = { - app: this.app, - feature: registeredCommand.feature + const context: CommandExecutionContext = { + app: this.app }; - await registeredCommand.command.execute(context, interaction); + await command.execute(context, interaction); } private async handleAutocomplete(interaction: APIApplicationCommandAutocompleteInteraction) { - const registeredCommand = this.app.registry.commands.get(interaction.data.name); + const command = this.app.registry.commands.get(interaction.data.name); - if (!registeredCommand?.command.autocomplete) { + if (!command?.autocomplete) { return; } - const context: FeatureContext = { - app: this.app, - feature: registeredCommand.feature + const context: CommandExecutionContext = { + app: this.app }; - await registeredCommand.command.autocomplete(context, interaction); + await command.autocomplete(context, interaction); } private async handleMessageComponent(interaction: APIMessageComponentInteraction) { diff --git a/src/core/types.ts b/src/core/types.ts index d3ec0e6c..454d3f73 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -8,6 +8,7 @@ import type { } from "@discordjs/core"; import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord-api-types/v10"; import type { drizzle } from "drizzle-orm/libsql"; +import type { AnyVersionedConfig } from "@/config/index"; import type { ParsedCustomId } from "@/core/custom-id"; import type { Logger } from "@/core/logger"; @@ -17,25 +18,19 @@ export type RoutedInteraction = | APIMessageComponentInteraction | APIModalSubmitInteraction; -export interface SlashCommandDefinition { +export interface CommandModule { data: RESTPostAPIChatInputApplicationCommandsJSONBody; - execute(context: FeatureContext, interaction: APIChatInputApplicationCommandInteraction): Promise; - autocomplete?(context: FeatureContext, interaction: APIApplicationCommandAutocompleteInteraction): Promise; + execute(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction): Promise; + autocomplete?(context: CommandExecutionContext, interaction: APIApplicationCommandAutocompleteInteraction): Promise; } export interface FeatureModule { key: string; - commands?: SlashCommandDefinition[]; buttons?: Record; stringSelects?: Record; modals?: Record; } -export interface RegisteredCommand { - command: SlashCommandDefinition; - feature: FeatureModule; -} - export interface EventModule { name: string; once?: boolean; @@ -45,7 +40,7 @@ export interface EventModule { export interface HandlerRegistry { events: EventModule[]; features: Map; - commands: Map; + commands: Map; applicationCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[]; } @@ -56,6 +51,7 @@ export interface InteractionRouterContract { export interface BotApp { client: Client; db: ReturnType; + config: AnyVersionedConfig; applicationId: string; logger: Logger; registry: HandlerRegistry; @@ -67,6 +63,10 @@ export interface FeatureContext { feature: FeatureModule; } +export interface CommandExecutionContext { + app: BotApp; +} + export interface ComponentExecutionContext extends FeatureContext { route: ParsedCustomId; } diff --git a/src/db/schema.ts b/src/db/schema.ts index 7b50448d..492fcbf9 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -2,6 +2,13 @@ import { int, sqliteTable, text } from "drizzle-orm/sqlite-core"; // NOTE: Columns that does not have notNull constraint ARE nullable. +export const panelMessagesTable = sqliteTable("panel_messages", { + panelKey: text().primaryKey(), + channelId: text().notNull(), + messageId: text().notNull(), + updatedAt: int().notNull() +}); + export const ticketsTable = sqliteTable("tickets", { id: int().primaryKey({ autoIncrement: true }), /** The ID of the channel where the ticket was created. */ @@ -19,9 +26,12 @@ export const ticketsTable = sqliteTable("tickets", { /** UNIX time */ claimedAt: int(), claimedBy: text(), + invitedUserIds: text().notNull().default("[]"), /** UNIX time */ closedAt: int(), closedBy: text(), closedReason: text(), transcriptUrl: text() }); + +export type TicketRecord = typeof ticketsTable.$inferSelect; diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index eff752f1..0d6990be 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -1,7 +1,7 @@ import { API } from "@discordjs/core"; import { REST } from "@discordjs/rest"; import { config } from "dotenv"; -import { discoverEvents, discoverFeatures } from "@/core/discovery"; +import { discoverCommands, discoverEvents, discoverFeatures } from "@/core/discovery"; import { createLogger } from "@/core/logger"; import { createHandlerRegistry } from "@/core/registry"; import botConfig from "../config/config.ts"; @@ -12,8 +12,12 @@ const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); const api = new API(rest); async function deployCommands() { - const [events, features] = await Promise.all([discoverEvents(logger), discoverFeatures(logger)]); - const registry = createHandlerRegistry({ features, events, logger }); + const [commands, events, features] = await Promise.all([ + discoverCommands(logger), + discoverEvents(logger), + discoverFeatures(logger) + ]); + const registry = createHandlerRegistry({ commands, features, events, logger }); if (botConfig.guildId) { await api.applicationCommands.bulkOverwriteGuildCommands(botConfig.clientId, botConfig.guildId, registry.applicationCommands); From 899828c178afdf332039f7f3f57f720c41b3e6a8 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:06:12 +0200 Subject: [PATCH 08/67] feat(tickets): implement interactive panel workflows * Sync visually generated panels dynamically upon startup * Route modal and select interactions for customizable ticket questions * Define /claim, /massadd, and /close slash command controls * Create comprehensive transcript backups synchronously on close * Enforce 25 user limitation upon ticket channels --- messages/tickets/open-panel.ts | 50 +++ messages/tickets/ticket-closed-dm.ts | 35 ++ messages/tickets/ticket-closed.ts | 50 +++ messages/tickets/ticket-opened.ts | 37 ++ src/events/ready.ts | 3 + src/features/commands/add/command.ts | 82 ++++ src/features/commands/claim/command.ts | 10 + src/features/commands/cleardm/command.ts | 60 +++ src/features/commands/close/command.ts | 10 + src/features/commands/massadd/command.ts | 132 +++++++ src/features/commands/remove/command.ts | 163 ++++++++ src/features/commands/rename/command.ts | 62 +++ src/features/commands/shared/options.ts | 29 ++ src/features/commands/unclaim/command.ts | 10 + src/features/tickets/claim-workflow.ts | 242 ++++++++++++ src/features/tickets/close-workflow.ts | 475 +++++++++++++++++++++++ src/features/tickets/config-access.ts | 60 +++ src/features/tickets/constants.ts | 11 + src/features/tickets/feature.ts | 27 ++ src/features/tickets/messages.ts | 421 ++++++++++++++++++++ src/features/tickets/panel-sync.ts | 358 +++++++++++++++++ src/features/tickets/participants.ts | 46 +++ src/features/tickets/records.ts | 44 +++ src/features/tickets/service.ts | 2 + src/features/tickets/ticket-workflow.ts | 362 +++++++++++++++++ src/features/tickets/transcripts.ts | 181 +++++++++ src/features/tickets/types.ts | 41 ++ src/features/tickets/utils.ts | 87 +++++ 28 files changed, 3090 insertions(+) create mode 100644 messages/tickets/open-panel.ts create mode 100644 messages/tickets/ticket-closed-dm.ts create mode 100644 messages/tickets/ticket-closed.ts create mode 100644 messages/tickets/ticket-opened.ts create mode 100644 src/features/commands/add/command.ts create mode 100644 src/features/commands/claim/command.ts create mode 100644 src/features/commands/cleardm/command.ts create mode 100644 src/features/commands/close/command.ts create mode 100644 src/features/commands/massadd/command.ts create mode 100644 src/features/commands/remove/command.ts create mode 100644 src/features/commands/rename/command.ts create mode 100644 src/features/commands/shared/options.ts create mode 100644 src/features/commands/unclaim/command.ts create mode 100644 src/features/tickets/claim-workflow.ts create mode 100644 src/features/tickets/close-workflow.ts create mode 100644 src/features/tickets/config-access.ts create mode 100644 src/features/tickets/constants.ts create mode 100644 src/features/tickets/feature.ts create mode 100644 src/features/tickets/messages.ts create mode 100644 src/features/tickets/panel-sync.ts create mode 100644 src/features/tickets/participants.ts create mode 100644 src/features/tickets/records.ts create mode 100644 src/features/tickets/service.ts create mode 100644 src/features/tickets/ticket-workflow.ts create mode 100644 src/features/tickets/transcripts.ts create mode 100644 src/features/tickets/types.ts create mode 100644 src/features/tickets/utils.ts diff --git a/messages/tickets/open-panel.ts b/messages/tickets/open-panel.ts new file mode 100644 index 00000000..07d1b1dc --- /dev/null +++ b/messages/tickets/open-panel.ts @@ -0,0 +1,50 @@ +import { ComponentType } from "discord-api-types/v10"; +import { createPanelOpenerSlot } from "@/features/tickets/messages"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +// Classic embed + components version: + +// const openPanelMessage: LoadedMessageTemplate = { +// useComponentsV2: false, +// embeds: [ +// { +// title: "Open a Ticket", +// description: "Choose the category that matches your request and the bot will create a private ticket for you.", +// color: 16106539 +// } +// ], +// components: [createPanelOpenerSlot()] +// }; + +// Components V2 version: + +const openPanelMessage: LoadedMessageTemplate = { + useComponentsV2: true, + components: [ + { + type: ComponentType.Container, + accent_color: 16106539, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Open a Ticket" + }, + { + type: ComponentType.TextDisplay, + content: "Choose the category that matches your request and the bot will create a private ticket for you." + }, + { + type: ComponentType.Separator + }, + { + type: ComponentType.Separator, + spacing: 1, + divider: false + }, + createPanelOpenerSlot() + ] + } + ] +}; + +export default openPanelMessage; diff --git a/messages/tickets/ticket-closed-dm.ts b/messages/tickets/ticket-closed-dm.ts new file mode 100644 index 00000000..4c2e486d --- /dev/null +++ b/messages/tickets/ticket-closed-dm.ts @@ -0,0 +1,35 @@ +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketClosedDmMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 16106539, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Your ticket has been closed" + }, + { + type: ComponentType.TextDisplay, + content: "**Reason**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Claim**\n{claimStatus}" + }, + { + type: ComponentType.TextDisplay, + content: "**Transcript**\n{transcriptStatus}" + }, + { + type: ComponentType.TextDisplay, + content: "_Closed by {closerName}_" + } + ] + } + ] +}; + +export default ticketClosedDmMessage; diff --git a/messages/tickets/ticket-closed.ts b/messages/tickets/ticket-closed.ts new file mode 100644 index 00000000..09c1147e --- /dev/null +++ b/messages/tickets/ticket-closed.ts @@ -0,0 +1,50 @@ +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketClosedMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 16106539, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Ticket Closed" + }, + { + type: ComponentType.TextDisplay, + content: "<@{userId}>'s ticket has been closed." + }, + { + type: ComponentType.TextDisplay, + content: "**Reason**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Claim**\n{claimStatus}" + }, + { + type: ComponentType.TextDisplay, + content: "**Transcript**\n{transcriptStatus}" + }, + { + type: ComponentType.TextDisplay, + content: "_Closed by {closerName}_" + }, + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + custom_id: "{deleteButtonCustomId}", + label: "Delete Ticket", + style: 4 + } + ] + } + ] + } + ] +}; + +export default ticketClosedMessage; diff --git a/messages/tickets/ticket-opened.ts b/messages/tickets/ticket-opened.ts new file mode 100644 index 00000000..50173979 --- /dev/null +++ b/messages/tickets/ticket-opened.ts @@ -0,0 +1,37 @@ +import { ComponentType } from "@discordjs/core"; +import { createMessageSlot } from "@/features/tickets/messages"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketOpenedMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.TextDisplay, + content: "{createdByMention}" + }, + { + type: ComponentType.Container, + accent_color: 16106539, + components: [ + { + type: ComponentType.TextDisplay, + content: "## {ticketTypeName} Ticket" + }, + { + type: ComponentType.TextDisplay, + content: "Thanks for opening a ticket." + }, + { + type: ComponentType.TextDisplay, + content: "**Details**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Claim Status**\n{claimStatus}" + }, + createMessageSlot("actions") + ] + } + ] +}; + +export default ticketOpenedMessage; diff --git a/src/events/ready.ts b/src/events/ready.ts index b1ce24f1..aec6a364 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,11 +1,14 @@ import { GatewayDispatchEvents, type GatewayReadyDispatchData, type ToEventProps } from "@discordjs/core"; import { defineEvent } from "@/core/defineEvent"; +import { syncTicketPanels } from "@/features/tickets/service"; const readyEvent = defineEvent<[ToEventProps]>({ name: GatewayDispatchEvents.Ready, once: true, async execute(app, event) { app.logger.info(`Connected as ${event.data.user.username}.`); + + await syncTicketPanels(app); } }); diff --git a/src/features/commands/add/command.ts b/src/features/commands/add/command.ts new file mode 100644 index 00000000..b178a774 --- /dev/null +++ b/src/features/commands/add/command.ts @@ -0,0 +1,82 @@ +import { MessageFlags } from "@discordjs/core"; +import { ApplicationCommandOptionType } from "discord-api-types/v10"; +import { getUserOption } from "@/features/commands/shared/options"; +import { defineCommand } from "@/core/defineCommand"; +import { reply } from "@/core/respond"; +import { + getInvitedUserIds, + grantTicketParticipantAccess, + MAX_INVITED_TICKET_USERS, + updateInvitedUserIds +} from "@/features/tickets/participants"; +import { getOpenTicketByChannel } from "@/features/tickets/records"; + +export default defineCommand({ + data: { + name: "add", + description: "Add someone to the current ticket", + options: [ + { + name: "user", + description: "The user to add", + required: true, + type: ApplicationCommandOptionType.User + } + ] + }, + async execute({ app }, interaction) { + const selectedUser = getUserOption(interaction, "user"); + + if (!selectedUser) { + await reply(app, interaction, { + content: "Choose a user to add to this ticket.", + flags: MessageFlags.Ephemeral + }); + return; + } + + const openTicket = await getOpenTicketByChannel(app, interaction.channel_id); + + if (!openTicket.ok) { + await reply(app, interaction, { + content: openTicket.message, + flags: MessageFlags.Ephemeral + }); + return; + } + + const invitedUserIds = getInvitedUserIds(openTicket.ticket); + + if (selectedUser.userId === openTicket.ticket.createdBy) { + await reply(app, interaction, { + content: "That user already has access to this ticket.", + flags: MessageFlags.Ephemeral + }); + return; + } + + if (invitedUserIds.includes(selectedUser.userId)) { + await reply(app, interaction, { + content: "That user is already invited to this ticket.", + flags: MessageFlags.Ephemeral + }); + return; + } + + if (invitedUserIds.length >= MAX_INVITED_TICKET_USERS) { + await reply(app, interaction, { + content: `You cannot invite more than ${MAX_INVITED_TICKET_USERS} users to one ticket.`, + flags: MessageFlags.Ephemeral + }); + return; + } + + await grantTicketParticipantAccess(app, openTicket.ticket.channelId, selectedUser.userId); + await updateInvitedUserIds(app, openTicket.ticket.channelId, [...invitedUserIds, selectedUser.userId]); + + await reply(app, interaction, { + content: `Added <@${selectedUser.userId}> to this ticket.`, + flags: MessageFlags.Ephemeral + }); + } +}); diff --git a/src/features/commands/claim/command.ts b/src/features/commands/claim/command.ts new file mode 100644 index 00000000..76eccc87 --- /dev/null +++ b/src/features/commands/claim/command.ts @@ -0,0 +1,10 @@ +import { defineCommand } from "@/core/defineCommand"; +import { executeClaimCommand } from "@/features/tickets/claim-workflow"; + +export default defineCommand({ + data: { + name: "claim", + description: "Claim the current ticket" + }, + execute: executeClaimCommand +}); diff --git a/src/features/commands/cleardm/command.ts b/src/features/commands/cleardm/command.ts new file mode 100644 index 00000000..b10bab9c --- /dev/null +++ b/src/features/commands/cleardm/command.ts @@ -0,0 +1,60 @@ +import { MessageFlags } from "@discordjs/core"; +import { defineCommand } from "@/core/defineCommand"; +import { editReply, reply } from "@/core/respond"; +import { getInteractionUser } from "@/features/tickets/utils"; + +export default defineCommand({ + data: { + name: "cleardm", + description: "Clear the bot's ticket history from your DMs" + }, + async execute({ app }, interaction) { + await reply(app, interaction, { + content: "Clearing your ticket DM history...", + flags: MessageFlags.Ephemeral + }); + + const user = getInteractionUser(interaction); + const dmChannel = await app.client.api.users.createDM(user.id).catch(() => null); + + if (!dmChannel?.id) { + await editReply(app, interaction, { + content: "I could not access your DM channel." + }); + return; + } + + let before: string | undefined; + let deletedCount = 0; + + while (true) { + const batch = await app.client.api.channels.getMessages(dmChannel.id, { + limit: 100, + before + }); + + if (batch.length === 0) { + break; + } + + for (const message of batch) { + if (message.author.id !== app.applicationId) { + continue; + } + + await app.client.api.channels.deleteMessage(dmChannel.id, message.id).catch(() => undefined); + deletedCount += 1; + } + + if (batch.length < 100) { + break; + } + + before = batch[batch.length - 1]?.id; + } + + await editReply(app, interaction, { + content: deletedCount > 0 ? `Cleared ${deletedCount} ticket DM messages.` : "No ticket DM messages were found." + }); + } +}); diff --git a/src/features/commands/close/command.ts b/src/features/commands/close/command.ts new file mode 100644 index 00000000..be5ae6a3 --- /dev/null +++ b/src/features/commands/close/command.ts @@ -0,0 +1,10 @@ +import { defineCommand } from "@/core/defineCommand"; +import { executeCloseCommand } from "@/features/tickets/close-workflow"; + +export default defineCommand({ + data: { + name: "close", + description: "Close the current ticket" + }, + execute: executeCloseCommand +}); diff --git a/src/features/commands/massadd/command.ts b/src/features/commands/massadd/command.ts new file mode 100644 index 00000000..a920b90b --- /dev/null +++ b/src/features/commands/massadd/command.ts @@ -0,0 +1,132 @@ +import { MessageFlags } from "@discordjs/core"; +import { ApplicationCommandOptionType } from "discord-api-types/v10"; +import { getStringOption } from "@/features/commands/shared/options"; +import { defineCommand } from "@/core/defineCommand"; +import { reply } from "@/core/respond"; +import { + getInvitedUserIds, + grantTicketParticipantAccess, + MAX_INVITED_TICKET_USERS, + updateInvitedUserIds +} from "@/features/tickets/participants"; +import { getOpenTicketByChannel } from "@/features/tickets/records"; + +export default defineCommand({ + data: { + name: "massadd", + description: "Add multiple users to the current ticket", + options: [ + { + name: "users", + description: "Comma-separated user IDs or mentions", + required: true, + type: ApplicationCommandOptionType.String + } + ] + }, + async execute({ app }, interaction) { + const rawValue = getStringOption(interaction, "users"); + const requestedUserIds = parseRequestedUserIds(rawValue ?? ""); + + if (requestedUserIds.length === 0) { + await reply(app, interaction, { + content: "Provide at least one user ID or mention.", + flags: MessageFlags.Ephemeral + }); + return; + } + + const openTicket = await getOpenTicketByChannel(app, interaction.channel_id); + + if (!openTicket.ok) { + await reply(app, interaction, { + content: openTicket.message, + flags: MessageFlags.Ephemeral + }); + return; + } + + const invitedUserIds = getInvitedUserIds(openTicket.ticket); + const nextInvitedUserIds = [...invitedUserIds]; + const addedUserIds: string[] = []; + const invalidUserIds: string[] = []; + const skippedUserIds: string[] = []; + let limitReached = false; + + for (const userId of requestedUserIds) { + if (userId === openTicket.ticket.createdBy || nextInvitedUserIds.includes(userId)) { + skippedUserIds.push(userId); + continue; + } + + if (nextInvitedUserIds.length >= MAX_INVITED_TICKET_USERS) { + limitReached = true; + break; + } + + const user = await app.client.api.users.get(userId).catch(() => null); + + if (!user) { + invalidUserIds.push(userId); + continue; + } + + await grantTicketParticipantAccess(app, openTicket.ticket.channelId, userId); + nextInvitedUserIds.push(userId); + addedUserIds.push(userId); + } + + if (addedUserIds.length > 0) { + await updateInvitedUserIds(app, openTicket.ticket.channelId, nextInvitedUserIds); + } + + await reply(app, interaction, { + content: buildMassAddSummary(addedUserIds, skippedUserIds, invalidUserIds, limitReached), + flags: MessageFlags.Ephemeral + }); + } +}); + +function parseRequestedUserIds(rawValue: string) { + const segments = rawValue + .split(",") + .map((segment) => segment.trim()) + .filter(Boolean); + + const requestedUserIds: string[] = []; + + for (const segment of segments) { + const mentionMatch = segment.match(/^<@!?(\d+)>$/); + const userId = mentionMatch?.[1] ?? (/^\d+$/.test(segment) ? segment : null); + + if (userId) { + requestedUserIds.push(userId); + } + } + + return [...new Set(requestedUserIds)]; +} + +function buildMassAddSummary(addedUserIds: string[], skippedUserIds: string[], invalidUserIds: string[], limitReached: boolean) { + const lines: string[] = []; + + if (addedUserIds.length > 0) { + lines.push(`Added ${addedUserIds.map((userId) => `<@${userId}>`).join(", ")}.`); + } else { + lines.push("No users were added."); + } + + if (skippedUserIds.length > 0) { + lines.push(`Skipped ${skippedUserIds.length} user(s) that already had access.`); + } + + if (invalidUserIds.length > 0) { + lines.push(`Skipped ${invalidUserIds.length} invalid user ID(s).`); + } + + if (limitReached) { + lines.push(`Stopped when the ${MAX_INVITED_TICKET_USERS}-user ticket limit was reached.`); + } + + return lines.join("\n"); +} diff --git a/src/features/commands/remove/command.ts b/src/features/commands/remove/command.ts new file mode 100644 index 00000000..8e9b04e1 --- /dev/null +++ b/src/features/commands/remove/command.ts @@ -0,0 +1,163 @@ +import type { APIChatInputApplicationCommandInteraction, APIMessageComponentInteraction } from "@discordjs/core"; +import { ComponentType, MessageFlags } from "@discordjs/core"; +import { ApplicationCommandOptionType } from "discord-api-types/v10"; +import { getUserOption } from "@/features/commands/shared/options"; +import { createCustomId } from "@/core/custom-id"; +import { defineCommand } from "@/core/defineCommand"; +import { reply, updateMessage } from "@/core/respond"; +import type { ComponentExecutionContext } from "@/core/types"; +import { getInvitedUserIds, revokeTicketParticipantAccess, updateInvitedUserIds } from "@/features/tickets/participants"; +import { getOpenTicketByChannel } from "@/features/tickets/records"; + +const REMOVE_USERS_CUSTOM_ID = createCustomId("tickets", "remove-users"); + +export default defineCommand({ + data: { + name: "remove", + description: "Remove invited users from the current ticket", + options: [ + { + name: "user", + description: "The invited user to remove immediately", + required: false, + type: ApplicationCommandOptionType.User + } + ] + }, + async execute({ app }, interaction) { + const openTicket = await getOpenTicketByChannel(app, interaction.channel_id); + + if (!openTicket.ok) { + await reply(app, interaction, { + content: openTicket.message, + flags: MessageFlags.Ephemeral + }); + return; + } + + const invitedUserIds = getInvitedUserIds(openTicket.ticket); + + if (invitedUserIds.length === 0) { + await reply(app, interaction, { + content: "There are no invited users to remove from this ticket.", + flags: MessageFlags.Ephemeral + }); + return; + } + + const selectedUser = getUserOption(interaction, "user"); + + if (selectedUser) { + await removeUsersFromTicket(app, interaction, invitedUserIds, openTicket.ticket.channelId, [selectedUser.userId]); + return; + } + + const options = await Promise.all( + invitedUserIds.map(async (userId) => { + const user = await app.client.api.users.get(userId).catch(() => null); + return { + label: user ? `${user.username}`.slice(0, 100) : userId, + value: userId + }; + }) + ); + + await reply(app, interaction, { + content: "Select the invited users you want to remove from this ticket.", + flags: MessageFlags.Ephemeral, + components: [ + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.StringSelect, + custom_id: REMOVE_USERS_CUSTOM_ID, + placeholder: "Choose users to remove", + min_values: 1, + max_values: options.length, + options + } + ] + } + ] + }); + } +}); + +export async function handleRemoveUsersSelect(context: ComponentExecutionContext, interaction: APIMessageComponentInteraction) { + if (interaction.data.component_type !== ComponentType.StringSelect) { + return; + } + + const openTicket = await getOpenTicketByChannel(context.app, interaction.channel_id); + + if (!openTicket.ok) { + await updateMessage(context.app, interaction, { + content: openTicket.message, + components: [] + }); + return; + } + + const invitedUserIds = getInvitedUserIds(openTicket.ticket); + const selectedUserIds = interaction.data.values.filter((userId: string) => invitedUserIds.includes(userId)); + + await removeUsersFromTicket(context.app, interaction, invitedUserIds, openTicket.ticket.channelId, selectedUserIds, { + responseMode: "update-message" + }); +} + +async function removeUsersFromTicket( + app: ComponentExecutionContext["app"], + interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction, + invitedUserIds: string[], + channelId: string, + selectedUserIds: string[], + options?: { + responseMode?: "reply" | "update-message"; + } +) { + const removableUserIds = selectedUserIds.filter((userId) => invitedUserIds.includes(userId)); + + if (removableUserIds.length === 0) { + await respond(app, interaction, "Those users are not invited to this ticket.", options?.responseMode); + return; + } + + for (const userId of removableUserIds) { + await revokeTicketParticipantAccess(app, channelId, userId); + } + + await updateInvitedUserIds( + app, + channelId, + invitedUserIds.filter((userId) => !removableUserIds.includes(userId)) + ); + + await respond( + app, + interaction, + `Removed ${removableUserIds.map((userId) => `<@${userId}>`).join(", ")} from this ticket.`, + options?.responseMode + ); +} + +async function respond( + app: ComponentExecutionContext["app"], + interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction, + content: string, + responseMode = "reply" +) { + if (responseMode === "update-message" && interaction.type === 3) { + await updateMessage(app, interaction, { + content, + components: [] + }); + return; + } + + await reply(app, interaction, { + content, + flags: MessageFlags.Ephemeral + }); +} diff --git a/src/features/commands/rename/command.ts b/src/features/commands/rename/command.ts new file mode 100644 index 00000000..ce69dcc4 --- /dev/null +++ b/src/features/commands/rename/command.ts @@ -0,0 +1,62 @@ +import { MessageFlags } from "@discordjs/core"; +import { ApplicationCommandOptionType } from "discord-api-types/v10"; +import { getStringOption } from "@/features/commands/shared/options"; +import { defineCommand } from "@/core/defineCommand"; +import { reply } from "@/core/respond"; +import { hasTicketStaffAccess } from "@/features/tickets/config-access"; +import { getOpenTicketByChannel } from "@/features/tickets/records"; +import { getMemberRoleIds, sanitizeChannelName } from "@/features/tickets/utils"; + +export default defineCommand({ + data: { + name: "rename", + description: "Rename the current ticket", + options: [ + { + name: "name", + description: "The new ticket channel name", + required: true, + type: ApplicationCommandOptionType.String + } + ] + }, + async execute({ app }, interaction) { + const openTicket = await getOpenTicketByChannel(app, interaction.channel_id); + + if (!openTicket.ok) { + await reply(app, interaction, { + content: openTicket.message, + flags: MessageFlags.Ephemeral + }); + return; + } + + if (!hasTicketStaffAccess(app, openTicket.ticketType, getMemberRoleIds(interaction))) { + await reply(app, interaction, { + content: "Only staff can rename this ticket.", + flags: MessageFlags.Ephemeral + }); + return; + } + + const requestedName = getStringOption(interaction, "name"); + + if (!requestedName) { + await reply(app, interaction, { + content: "Provide a new ticket name.", + flags: MessageFlags.Ephemeral + }); + return; + } + + const nextName = sanitizeChannelName(requestedName); + await app.client.api.channels.edit(openTicket.ticket.channelId, { + name: nextName + }); + + await reply(app, interaction, { + content: `Ticket renamed to <#${openTicket.ticket.channelId}>.`, + flags: MessageFlags.Ephemeral + }); + } +}); diff --git a/src/features/commands/shared/options.ts b/src/features/commands/shared/options.ts new file mode 100644 index 00000000..d074c195 --- /dev/null +++ b/src/features/commands/shared/options.ts @@ -0,0 +1,29 @@ +import type { APIChatInputApplicationCommandInteraction, APIUser } from "@discordjs/core"; + +type RawCommandOption = { + name: string; + value?: string; +}; + +function findOption(interaction: APIChatInputApplicationCommandInteraction, name: string) { + const options = (interaction.data.options ?? []) as RawCommandOption[]; + return options.find((option) => option.name === name); +} + +export function getStringOption(interaction: APIChatInputApplicationCommandInteraction, name: string) { + const value = findOption(interaction, name)?.value; + return typeof value === "string" ? value : null; +} + +export function getUserOption(interaction: APIChatInputApplicationCommandInteraction, name: string) { + const userId = getStringOption(interaction, name); + + if (!userId) { + return null; + } + + return { + user: interaction.data.resolved?.users?.[userId] as APIUser | undefined, + userId + }; +} diff --git a/src/features/commands/unclaim/command.ts b/src/features/commands/unclaim/command.ts new file mode 100644 index 00000000..93eafcf5 --- /dev/null +++ b/src/features/commands/unclaim/command.ts @@ -0,0 +1,10 @@ +import { defineCommand } from "@/core/defineCommand"; +import { executeUnclaimCommand } from "@/features/tickets/claim-workflow"; + +export default defineCommand({ + data: { + name: "unclaim", + description: "Unclaim the current ticket" + }, + execute: executeUnclaimCommand +}); diff --git a/src/features/tickets/claim-workflow.ts b/src/features/tickets/claim-workflow.ts new file mode 100644 index 00000000..67ca23ca --- /dev/null +++ b/src/features/tickets/claim-workflow.ts @@ -0,0 +1,242 @@ +import type { APIChatInputApplicationCommandInteraction, APIMessageComponentInteraction } from "@discordjs/core"; +import { MessageFlags } from "@discordjs/core"; +import { eq } from "drizzle-orm"; +import { reply } from "@/core/respond"; +import type { BotApp, CommandExecutionContext, ComponentExecutionContext } from "@/core/types"; +import { ticketsTable } from "@/db/schema"; +import { hasTicketStaffAccess } from "@/features/tickets/config-access"; +import { getOpenTicketByChannel } from "@/features/tickets/records"; +import { syncTicketWelcomeMessage } from "@/features/tickets/ticket-workflow"; +import { getInteractionUser, getMemberRoleIds, renderChannelName } from "@/features/tickets/utils"; + +type ClaimInteraction = APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction; + +export async function executeClaimCommand(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction) { + await claimTicket(context.app, interaction); +} + +export async function executeUnclaimCommand(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction) { + await unclaimTicket(context.app, interaction); +} + +export async function handleClaimButton(context: ComponentExecutionContext, interaction: APIMessageComponentInteraction) { + await claimTicket(context.app, interaction); +} + +export async function handleUnclaimButton(context: ComponentExecutionContext, interaction: APIMessageComponentInteraction) { + await unclaimTicket(context.app, interaction); +} + +async function claimTicket(app: BotApp, interaction: ClaimInteraction) { + if (!app.config.tickets.claims.enabled) { + await replyWithContent(app, interaction, "Ticket claiming is disabled."); + return; + } + + const actor = getInteractionUser(interaction); + const claimable = await getClaimableTicket(app, interaction.channel_id, getMemberRoleIds(interaction)); + + if (!claimable.ok) { + await replyWithContent(app, interaction, claimable.message); + return; + } + + const { ticket, ticketType } = claimable; + + if (ticket.claimedBy === actor.id) { + await replyWithContent(app, interaction, "You already claimed this ticket."); + return; + } + + if (ticket.claimedBy && !canTakeOverClaim(app, getMemberRoleIds(interaction))) { + await replyWithContent(app, interaction, "This ticket is already claimed and cannot be taken over."); + return; + } + + const claimedAt = Date.now(); + await app.db + .update(ticketsTable) + .set({ + claimedAt, + claimedBy: actor.id + }) + .where(eq(ticketsTable.channelId, ticket.channelId)); + + const nextTicketState = { + ...ticket, + claimedAt, + claimedBy: actor.id + }; + + await updateClaimedTicketPresentation(app, nextTicketState, ticketType.name, actor.username); + + await syncTicketWelcomeMessage( + app, + nextTicketState, + ticketType + ); + + await replyWithContent( + app, + interaction, + ticket.claimedBy ? `Ticket reassigned to <@${actor.id}>.` : `You claimed this ticket.` + ); +} + +async function unclaimTicket(app: BotApp, interaction: ClaimInteraction) { + if (!app.config.tickets.claims.enabled) { + await replyWithContent(app, interaction, "Ticket claiming is disabled."); + return; + } + + if (!app.config.tickets.claims.allowUnclaim) { + await replyWithContent(app, interaction, "Unclaiming is disabled for this server."); + return; + } + + const actor = getInteractionUser(interaction); + const claimable = await getClaimableTicket(app, interaction.channel_id, getMemberRoleIds(interaction)); + + if (!claimable.ok) { + await replyWithContent(app, interaction, claimable.message); + return; + } + + const { ticket, ticketType } = claimable; + + if (!ticket.claimedBy) { + await replyWithContent(app, interaction, "This ticket is not claimed."); + return; + } + + if (ticket.claimedBy !== actor.id) { + await replyWithContent(app, interaction, "Only the current claimer can unclaim this ticket."); + return; + } + + await app.db + .update(ticketsTable) + .set({ + claimedAt: null, + claimedBy: null + }) + .where(eq(ticketsTable.channelId, ticket.channelId)); + + await syncTicketWelcomeMessage( + app, + { + ...ticket, + claimedAt: null, + claimedBy: null + }, + ticketType + ); + + await replyWithContent(app, interaction, "You unclaimed this ticket."); +} + +async function getClaimableTicket(app: BotApp, channelId: string | undefined, roleIds: string[]) { + const openTicket = await getOpenTicketByChannel(app, channelId); + + if (!openTicket.ok) { + return openTicket; + } + + const { ticket, ticketType } = openTicket; + + if (!hasTicketStaffAccess(app, ticketType, roleIds)) { + return { + ok: false as const, + message: "Only staff can claim this ticket." + }; + } + + return { + ok: true as const, + ticket, + ticketType + }; +} + +function canTakeOverClaim(app: BotApp, roleIds: string[]) { + switch (app.config.tickets.claims.takeoverMode) { + case "staff": + return true; + case "roles": { + const allowedRoleIds = new Set(app.config.tickets.claims.takeoverRoleIds ?? []); + return roleIds.some((roleId) => allowedRoleIds.has(roleId)); + } + default: + return false; + } +} + +async function updateClaimedTicketPresentation( + app: BotApp, + ticket: { + channelId: string; + claimedBy: string | null; + claimedAt: number | null; + createdBy: string; + id: number; + type: string; + }, + ticketTypeName: string, + claimerUsername: string +) { + await renameClaimedTicketChannel(app, ticket, ticketTypeName, claimerUsername); + await moveClaimedTicketChannel(app, ticket.channelId); +} + +async function renameClaimedTicketChannel( + app: BotApp, + ticket: { + channelId: string; + claimedBy: string | null; + createdBy: string; + id: number; + type: string; + }, + ticketTypeName: string, + claimerUsername: string +) { + const template = app.config.tickets.claims.nameWhenClaimed?.trim(); + + if (!template || !ticket.claimedBy) { + return; + } + + const creator = await app.client.api.users.get(ticket.createdBy).catch(() => null); + const nextName = renderChannelName(template, { + claimerId: ticket.claimedBy, + claimerUsername, + createdById: ticket.createdBy, + createdByUsername: creator?.username ?? ticket.createdBy, + ticketNumber: ticket.id.toString(), + ticketTypeKey: ticket.type, + ticketTypeName + }); + + await app.client.api.channels.edit(ticket.channelId, { + name: nextName + }); +} + +async function moveClaimedTicketChannel(app: BotApp, channelId: string) { + const categoryId = app.config.tickets.claims.categoryWhenClaimed?.trim(); + + if (!categoryId) { + return; + } + + await app.client.api.channels.edit(channelId, { + parent_id: categoryId + }); +} + +async function replyWithContent(app: BotApp, interaction: ClaimInteraction, content: string) { + await reply(app, interaction, { + content, + flags: MessageFlags.Ephemeral + }); +} diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts new file mode 100644 index 00000000..8de07881 --- /dev/null +++ b/src/features/tickets/close-workflow.ts @@ -0,0 +1,475 @@ +import type { + APIButtonComponentWithCustomId, + APIChatInputApplicationCommandInteraction, + APIMessage, + APIMessageComponentInteraction, + APIModalSubmitInteraction +} from "@discordjs/core"; +import { ButtonStyle, ComponentType, MessageFlags, TextInputStyle } from "@discordjs/core"; +import { eq } from "drizzle-orm"; +import { createCustomId } from "@/core/custom-id"; +import { editReply, reply, showModal } from "@/core/respond"; +import type { BotApp, CommandExecutionContext, ComponentExecutionContext } from "@/core/types"; +import { ticketsTable } from "@/db/schema"; +import { getTicketType, hasTicketStaffAccess } from "@/features/tickets/config-access"; +import { appendMessageButton, finalizeMessageTemplate, hasMessageComponentCustomId, loadMessageTemplate } from "@/features/tickets/messages"; +import { getInvitedUserIds, revokeTicketParticipantAccess } from "@/features/tickets/participants"; +import { findTicketByChannel, getOpenTicketByChannel } from "@/features/tickets/records"; +import { startTranscriptJob } from "@/features/tickets/transcripts"; +import { getInteractionUser, getMemberRoleIds } from "@/features/tickets/utils"; + +const DEFAULT_CLOSE_DM_MESSAGE = "tickets/ticket-closed-dm"; +const DEFAULT_CLOSE_CHANNEL_MESSAGE = "tickets/ticket-closed"; + +export async function executeCloseCommand(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction) { + await beginCloseFlow(context.app, interaction); +} + +export async function handleCloseButton(context: ComponentExecutionContext, interaction: APIMessageComponentInteraction) { + await beginCloseFlow(context.app, interaction); +} + +export async function handleDeleteClosedTicketButton( + context: ComponentExecutionContext, + interaction: APIMessageComponentInteraction +) { + const channelId = interaction.channel_id; + + if (!channelId) { + await reply(context.app, interaction, { + content: "This interaction was not used in a ticket channel.", + flags: MessageFlags.Ephemeral + }); + return; + } + + const manageable = await getDeletableTicket(context.app, channelId, getMemberRoleIds(interaction)); + + if (!manageable.ok) { + await reply(context.app, interaction, { + content: manageable.message, + flags: MessageFlags.Ephemeral + }); + return; + } + + await reply(context.app, interaction, { + content: "Deleting ticket channel...", + flags: MessageFlags.Ephemeral + }); + await context.app.client.api.channels.delete(channelId); +} + +export async function handleCloseReasonSubmit(context: ComponentExecutionContext, interaction: APIModalSubmitInteraction) { + const reason = readCloseReason(interaction); + await closeTicket(context.app, interaction, reason); +} + +async function beginCloseFlow( + app: BotApp, + interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction +) { + const closable = await getClosableTicket(app, interaction.channel_id, getMemberRoleIds(interaction), getInteractionUser(interaction).id, true); + + if (!closable.ok) { + await reply(app, interaction, { + content: closable.message, + flags: MessageFlags.Ephemeral + }); + return; + } + + if (app.config.tickets.close.askForReason) { + await showModal(app, interaction, { + custom_id: createCustomId("tickets", "submit-close-reason"), + title: "Close Ticket", + components: [ + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.TextInput, + custom_id: "reason", + label: "Reason", + style: TextInputStyle.Paragraph, + required: false, + max_length: 500, + placeholder: "Why is this ticket being closed?" + } + ] + } + ] + }); + return; + } + + await closeTicket(app, interaction, null); +} + +async function closeTicket( + app: BotApp, + interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction | APIModalSubmitInteraction, + reason: string | null +) { + const channelId = interaction.channel_id; + + if (!channelId) { + await reply(app, interaction, { + content: "This interaction was not used in a ticket channel.", + flags: MessageFlags.Ephemeral + }); + return; + } + + const roleIds = getMemberRoleIds(interaction); + const closable = await getClosableTicket(app, channelId, roleIds, getInteractionUser(interaction).id, true); + + if (!closable.ok) { + await reply(app, interaction, { + content: closable.message, + flags: MessageFlags.Ephemeral + }); + return; + } + + const status = createCloseStatusUpdater(app, interaction); + await status.start(app.config.tickets.close.createTranscript ? "Preparing transcript..." : "Closing ticket..."); + + const { ticket } = closable; + const closer = getInteractionUser(interaction); + const normalizedReason = normalizeCloseReason(reason); + + // Mark the ticket as closed immediately so repeated button presses or `/close` + // attempts during transcript generation do not start duplicate close flows. + await app.db + .update(ticketsTable) + .set({ + closedAt: Date.now(), + closedBy: closer.id, + closedReason: normalizedReason + }) + .where(eq(ticketsTable.channelId, ticket.channelId)); + + if (!app.config.tickets.close.deleteChannelOnClose) { + await status.update("Updating ticket access..."); + await revokeTicketParticipantAccess(app, ticket.channelId, ticket.createdBy); + + for (const invitedUserId of getInvitedUserIds(ticket)) { + await revokeTicketParticipantAccess(app, ticket.channelId, invitedUserId); + } + } + + // Keep the original ticket message in sync with the closed state when the + // channel is preserved for staff review. + await disableTicketActionButtons(app, ticket.channelId, ticket.creationMessageId); + + if (!app.config.tickets.close.deleteChannelOnClose) { + await moveClosedTicketChannel(app, ticket.channelId); + } + + const transcriptJob = app.config.tickets.close.createTranscript + ? await startTranscriptJob(app, ticket.channelId, { + onStatus: (content) => status.update(content) + }) + : null; + const transcriptUrl = transcriptJob ? await transcriptJob.waitForResult() : null; + + if (app.config.tickets.close.createTranscript && !transcriptUrl) { + await status.update("Transcript is still processing. Finishing ticket close..."); + } + + const closeMessageTokens = { + channelId: ticket.channelId, + claimStatus: formatClaimStatus(ticket.claimedBy), + claimerId: ticket.claimedBy ?? "", + claimerMention: ticket.claimedBy ? `<@${ticket.claimedBy}>` : "", + closerId: closer.id, + closerMention: `<@${closer.id}>`, + closerName: closer.username, + reason: normalizedReason, + transcriptStatus: formatTranscriptStatus(transcriptUrl), + transcriptUrl: transcriptUrl ?? "", + userId: ticket.createdBy + }; + + if (app.config.tickets.close.dmUserOnClose) { + await status.update("Sending close confirmation..."); + await sendCloseDm(app, ticket.createdBy, closeMessageTokens); + } + + if (app.config.tickets.close.deleteChannelOnClose) { + await editReply(app, interaction, { + content: transcriptUrl + ? "Ticket closed. The transcript is ready and the channel will now be deleted." + : "Ticket closed. The channel will now be deleted." + }); + await app.client.api.channels.delete(ticket.channelId); + return; + } + + await status.update("Posting close summary..."); + await app.client.api.channels.createMessage(ticket.channelId, await buildCloseChannelMessage(app, closeMessageTokens)); + + await status.update("Ticket closed."); +} + +async function getClosableTicket( + app: BotApp, + channelId: string | undefined, + roleIds: string[], + actorId: string, + enforcePermission: boolean +) { + const openTicket = await getOpenTicketByChannel(app, channelId); + + if (!openTicket.ok) { + return openTicket; + } + + const { ticket, ticketType } = openTicket; + + if (enforcePermission && app.config.tickets.close.staffOnly && !hasTicketStaffAccess(app, ticketType, roleIds)) { + return { + ok: false as const, + message: "Only staff can close this ticket." + }; + } + + if (app.config.tickets.claims.enabled && app.config.tickets.claims.mode === "strict") { + if (!ticket.claimedBy) { + return { + ok: false as const, + message: "This ticket must be claimed before it can be closed." + }; + } + + if (ticket.claimedBy !== actorId) { + return { + ok: false as const, + message: "Only the current claimer can close this ticket." + }; + } + } + + return { + ok: true as const, + ticket, + ticketType + }; +} + +async function getDeletableTicket(app: BotApp, channelId: string | undefined, roleIds: string[]) { + if (!channelId) { + return { + ok: false as const, + message: "This interaction was not used in a ticket channel." + }; + } + + const ticket = await findTicketByChannel(app, channelId); + + if (!ticket) { + return { + ok: false as const, + message: "This channel is not a ticket." + }; + } + + if (!ticket.closedAt) { + return { + ok: false as const, + message: "Only closed tickets can be deleted from this button." + }; + } + + const ticketType = getTicketType(app, ticket.type); + + if (!hasTicketStaffAccess(app, ticketType, roleIds)) { + return { + ok: false as const, + message: "Only staff can delete this ticket." + }; + } + + return { + ok: true as const, + ticket, + ticketType + }; +} + +async function disableTicketActionButtons(app: BotApp, channelId: string, messageId: string) { + const message = await app.client.api.channels.getMessage(channelId, messageId).catch(() => null); + const disabledButtonIds = new Set([ + createCustomId("tickets", "claim"), + createCustomId("tickets", "close"), + createCustomId("tickets", "unclaim") + ]); + + if (!message?.components?.length) { + return; + } + + const nextComponents = message.components.map((row) => { + if (row.type !== ComponentType.ActionRow) { + return row; + } + + return { + ...row, + components: row.components.map((component) => { + if ( + component.type !== ComponentType.Button || + !("custom_id" in component) || + !disabledButtonIds.has(component.custom_id) + ) { + return component; + } + + return { + ...component, + disabled: true + }; + }) + }; + }) as APIMessage["components"]; + + await app.client.api.channels.editMessage(channelId, messageId, { + components: nextComponents + }); +} + +async function moveClosedTicketChannel(app: BotApp, channelId: string) { + const categoryId = app.config.tickets.close.closeTicketCategoryId?.trim(); + + if (!categoryId) { + return; + } + + await app.client.api.channels.edit(channelId, { + parent_id: categoryId + }); +} + +async function sendCloseDm( + app: BotApp, + userId: string, + tokens: { + channelId: string; + closerId: string; + closerMention: string; + closerName: string; + reason: string; + transcriptStatus: string; + transcriptUrl: string; + userId: string; + } +) { + const dmChannel = await app.client.api.users.createDM(userId).catch(() => null); + + if (!dmChannel?.id) { + return; + } + + const messageTemplate = await loadMessageTemplate(app.config.tickets.close.dmMessage ?? DEFAULT_CLOSE_DM_MESSAGE, tokens); + + await app.client.api.channels + .createMessage(dmChannel.id, { + ...finalizeMessageTemplate(messageTemplate) + }) + .catch(() => undefined); +} + +async function buildCloseChannelMessage( + app: BotApp, + tokens: { + channelId: string; + closerId: string; + closerMention: string; + closerName: string; + claimStatus: string; + claimerId: string; + claimerMention: string; + reason: string; + transcriptStatus: string; + transcriptUrl: string; + userId: string; + } +) { + const deleteButtonCustomId = createCustomId("tickets", "delete-closed"); + const messageTemplate = await loadMessageTemplate(app.config.tickets.close.channelMessage ?? DEFAULT_CLOSE_CHANNEL_MESSAGE, { + ...tokens, + deleteButtonCustomId + }); + + return finalizeMessageTemplate( + appendMessageButton( + messageTemplate, + !hasMessageComponentCustomId(messageTemplate, deleteButtonCustomId) + ? ({ + type: ComponentType.Button, + custom_id: deleteButtonCustomId, + label: "Delete Ticket", + style: ButtonStyle.Danger + } satisfies APIButtonComponentWithCustomId) + : undefined + ) + ); +} + +function createCloseStatusUpdater( + app: BotApp, + interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction | APIModalSubmitInteraction +) { + let hasStarted = false; + let lastContent = ""; + + return { + start: async (content: string) => { + hasStarted = true; + lastContent = content; + await reply(app, interaction, { + content, + flags: MessageFlags.Ephemeral + }); + }, + update: async (content: string) => { + if (!hasStarted || content === lastContent) { + return; + } + + lastContent = content; + await editReply(app, interaction, { + content + }).catch(() => undefined); + } + }; +} + +function formatTranscriptStatus(transcriptUrl: string | null) { + return transcriptUrl ? `[Open Transcript](${transcriptUrl})` : "Unavailable or still processing."; +} + +function formatClaimStatus(claimedBy: string | null) { + return claimedBy ? `Claimed by <@${claimedBy}>` : "Unclaimed"; +} + +function readCloseReason(interaction: APIModalSubmitInteraction) { + for (const component of interaction.data.components) { + if (!("components" in component)) { + continue; + } + + for (const child of component.components) { + if (child.type === ComponentType.TextInput && child.custom_id === "reason" && "value" in child) { + return child.value.trim() || null; + } + } + } + + return null; +} + +function normalizeCloseReason(reason: string | null) { + return reason?.trim() || "No reason provided."; +} diff --git a/src/features/tickets/config-access.ts b/src/features/tickets/config-access.ts new file mode 100644 index 00000000..9eed92b8 --- /dev/null +++ b/src/features/tickets/config-access.ts @@ -0,0 +1,60 @@ +import type { BotApp } from "@/core/types"; +import type { PanelConfig, TicketTypeConfig } from "@/features/tickets/types"; +import { isBlockedByRoles } from "@/features/tickets/utils"; + +export function getPanel(app: BotApp, panelKey: string) { + const panel = app.config.panels[panelKey]; + + if (!panel) { + throw new Error(`Unknown panel "${panelKey}".`); + } + + return panel; +} + +export function getTicketType(app: BotApp, ticketTypeKey: string) { + const ticketType = app.config.ticketTypes[ticketTypeKey]; + + if (!ticketType) { + throw new Error(`Unknown ticket type "${ticketTypeKey}".`); + } + + if (ticketType.openForm && ticketType.openForm.questions.length > 5) { + throw new Error(`Ticket type "${ticketTypeKey}" has more than 5 modal questions.`); + } + + return ticketType; +} + +export function getPanelTicketTypeKeys(panel: PanelConfig) { + return panel.opener.type === "buttons" ? panel.opener.buttons.map((button) => button.ticketType) : panel.opener.ticketTypes; +} + +export function userCanAccessTicketType(app: BotApp, ticketType: TicketTypeConfig, roleIds: string[]) { + return !isBlockedByRoles([...app.config.tickets.blockedRoleIds, ...(ticketType.blockedRoleIds ?? [])], roleIds); +} + +export function getTicketStaffRoleIds(app: BotApp, ticketType: TicketTypeConfig) { + return [...new Set([...app.config.tickets.staffRoleIds, ...(ticketType.staffRoleIds ?? [])])]; +} + +export function hasTicketStaffAccess(app: BotApp, ticketType: TicketTypeConfig, roleIds: string[]) { + const allowedRoleIds = new Set(getTicketStaffRoleIds(app, ticketType)); + return roleIds.some((roleId) => allowedRoleIds.has(roleId)); +} + +export function validatePanelConfig(app: BotApp, panelKey: string, panel: PanelConfig) { + const ticketTypeKeys = getPanelTicketTypeKeys(panel); + + for (const ticketTypeKey of ticketTypeKeys) { + getTicketType(app, ticketTypeKey); + } + + if (panel.opener.type === "buttons" && panel.opener.buttons.length > 25) { + throw new Error(`Panel "${panelKey}" has more than 25 buttons.`); + } + + if (panel.opener.type !== "buttons" && panel.opener.ticketTypes.length > 25) { + throw new Error(`Panel "${panelKey}" has more than 25 select options.`); + } +} diff --git a/src/features/tickets/constants.ts b/src/features/tickets/constants.ts new file mode 100644 index 00000000..ff8b63f9 --- /dev/null +++ b/src/features/tickets/constants.ts @@ -0,0 +1,11 @@ +import { PermissionFlagsBits } from "@discordjs/core"; + +export const MESSAGE_TEMPLATES_DIRECTORY = new URL("../../../messages", import.meta.url); +export const DEFAULT_NO_REASON = "No additional details were provided."; +export const TICKET_ACCESS_ALLOW = + PermissionFlagsBits.ViewChannel | + PermissionFlagsBits.SendMessages | + PermissionFlagsBits.ReadMessageHistory | + PermissionFlagsBits.AttachFiles | + PermissionFlagsBits.EmbedLinks | + PermissionFlagsBits.AddReactions; diff --git a/src/features/tickets/feature.ts b/src/features/tickets/feature.ts new file mode 100644 index 00000000..7921d0cb --- /dev/null +++ b/src/features/tickets/feature.ts @@ -0,0 +1,27 @@ +import { defineFeature } from "@/core/defineFeature"; +import { handleRemoveUsersSelect } from "@/features/commands/remove/command"; +import { handleClaimButton, handleUnclaimButton } from "@/features/tickets/claim-workflow"; +import { handleCloseButton, handleCloseReasonSubmit, handleDeleteClosedTicketButton } from "@/features/tickets/close-workflow"; +import { handleOpenFormSubmit, handleOpenPanelSelector, handlePanelButtons, handlePanelSelect } from "@/features/tickets/service"; + +const ticketsFeature = defineFeature({ + key: "tickets", + buttons: { + claim: handleClaimButton, + close: handleCloseButton, + "delete-closed": handleDeleteClosedTicketButton, + "open-select": handleOpenPanelSelector, + "open-type": handlePanelButtons, + unclaim: handleUnclaimButton + }, + stringSelects: { + "panel-select": handlePanelSelect, + "remove-users": handleRemoveUsersSelect + }, + modals: { + "submit-close-reason": handleCloseReasonSubmit, + "submit-open-form": handleOpenFormSubmit + } +}); + +export default ticketsFeature; diff --git a/src/features/tickets/messages.ts b/src/features/tickets/messages.ts new file mode 100644 index 00000000..b3af9df1 --- /dev/null +++ b/src/features/tickets/messages.ts @@ -0,0 +1,421 @@ +import { access } from "node:fs/promises"; +import { extname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import type { APIButtonComponentWithCustomId, APIMessageTopLevelComponent } from "@discordjs/core"; +import { ComponentType, MessageFlags } from "@discordjs/core"; +import { MESSAGE_TEMPLATES_DIRECTORY } from "@/features/tickets/constants"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import { renderTemplate } from "@/features/tickets/utils"; + +const TEMPLATE_SLOT_TYPE = "template-slot"; +const TEMPLATE_SLOT_KIND_MANY = "many"; +const COMPONENTS_V2_TYPES = new Set([ + ComponentType.Section, + ComponentType.TextDisplay, + ComponentType.Thumbnail, + ComponentType.MediaGallery, + ComponentType.File, + ComponentType.Separator, + ComponentType.Container +]); + +export function createMessageSlot(slot: string): any { + return { + type: TEMPLATE_SLOT_TYPE, + slot, + slot_kind: TEMPLATE_SLOT_KIND_MANY + }; +} + +export function createPanelOpenerSlot() { + return createMessageSlot("panel-opener"); +} + +export async function loadMessageTemplate( + reference: string, + tokens?: Record +): Promise { + const resolvedPath = await resolveMessageTemplatePath(reference); + const rawPayload = await loadMessageTemplateSource(resolvedPath); + const normalizedPayload = normalizeMessageTemplate(rawPayload); + const renderedPayload = tokens + ? (renderDeep(normalizedPayload, tokens) as LoadedMessageTemplate) + : (structuredClone(normalizedPayload) as LoadedMessageTemplate); + + return applyComponentsV2Defaults(renderedPayload); +} + +export function finalizeMessageTemplate(payload: LoadedMessageTemplate) { + return sanitizeMessageTemplate(applyComponentsV2Defaults(payload)); +} + +export function appendMessageText(payload: LoadedMessageTemplate, text: string | undefined) { + const normalizedText = text?.trim(); + + if (!normalizedText) { + return payload; + } + + if (usesComponentsV2(payload)) { + // Components V2 messages cannot use the legacy `content` field, so extra + // runtime text is injected as a text display block instead. + return appendMessageComponents(payload, [ + { + type: ComponentType.TextDisplay, + content: normalizedText + } + ]); + } + + return { + ...payload, + content: [payload.content, normalizedText].filter((part): part is string => Boolean(part?.trim())).join("\n").trim() + }; +} + +export function appendMessageComponents( + payload: LoadedMessageTemplate, + components: APIMessageTopLevelComponent[] | undefined, + slot = "actions" +): LoadedMessageTemplate { + if (!components?.length) { + return payload; + } + + const currentComponents = payload.components ?? []; + const injectedComponents = injectManyIntoSlots(currentComponents, components, slot); + const nextComponents = injectedComponents.replaced ? injectedComponents.value : [...currentComponents, ...components]; + + return { + ...payload, + components: nextComponents + }; +} + +export function appendMessageButton( + payload: LoadedMessageTemplate, + button: APIButtonComponentWithCustomId | undefined, + options?: { + actionSlot?: string; + } +): LoadedMessageTemplate { + if (!button) { + return payload; + } + + return appendMessageComponents( + payload, + [ + { + type: ComponentType.ActionRow, + components: [button] + } + ], + options?.actionSlot ?? "actions" + ); +} + +export function appendPanelOpener( + payload: LoadedMessageTemplate, + components: APIMessageTopLevelComponent[] | undefined +): LoadedMessageTemplate { + if (!components?.length) { + return payload; + } + + const currentComponents = payload.components ?? []; + const slottedInjection = injectManyIntoSlots(currentComponents, components, "panel-opener"); + + if (slottedInjection.replaced) { + return { + ...payload, + components: slottedInjection.value + }; + } + + if (usesComponentsV2(payload)) { + const containerInjection = appendComponentsToFirstContainer(currentComponents, components); + + if (containerInjection.replaced) { + return { + ...payload, + components: containerInjection.value + }; + } + } + + return appendMessageComponents(payload, components); +} + +export function hasMessageComponentCustomId(payload: LoadedMessageTemplate, customId: string) { + const visit = (value: unknown): boolean => { + if (Array.isArray(value)) { + return value.some((entry) => visit(entry)); + } + + if (!value || typeof value !== "object") { + return false; + } + + if ("custom_id" in value && value.custom_id === customId) { + return true; + } + + return Object.values(value).some((entry) => visit(entry)); + }; + + return visit(payload.components); +} + +function usesComponentsV2(payload: LoadedMessageTemplate) { + return payload.useComponentsV2 ?? (Boolean((payload.flags ?? 0) & MessageFlags.IsComponentsV2) || hasComponentsV2Components(payload.components)); +} + +async function resolveMessageTemplatePath(reference: string) { + const templatesDirectoryPath = resolve(fileURLToPath(MESSAGE_TEMPLATES_DIRECTORY)); + const normalizedReference = reference.replaceAll("\\", "/").replace(/^\/+/, ""); + const resolvedBasePath = resolve(templatesDirectoryPath, normalizedReference); + + if (!resolvedBasePath.startsWith(templatesDirectoryPath)) { + throw new Error(`Message template reference "${reference}" resolves outside the messages directory.`); + } + + const candidatePaths = extname(resolvedBasePath) === "" ? [`${resolvedBasePath}.ts`] : [resolvedBasePath]; + + for (const candidatePath of candidatePaths) { + try { + await access(candidatePath); + return candidatePath; + } catch {} + } + + throw new Error(`Message template "${reference}" was not found in ${templatesDirectoryPath}.`); +} + +async function loadMessageTemplateSource(filePath: string) { + const extension = extname(filePath).toLowerCase(); + + if (extension === ".ts") { + // Templates stay code-only in v4 so they remain typed and can opt into + // Components V2 without extra parsing layers. + const importedModule = await import(pathToFileURL(filePath).href); + return importedModule.default ?? importedModule.message ?? importedModule; + } + + throw new Error(`Unsupported template file type "${extension}". Only TypeScript templates are supported.`); +} + +function normalizeMessageTemplate(rawPayload: unknown): LoadedMessageTemplate { + if (!rawPayload || typeof rawPayload !== "object") { + throw new Error("Message templates must export a message payload object."); + } + + const payload = rawPayload as LoadedMessageTemplate; + + return { + content: payload.content, + embeds: payload.embeds, + components: payload.components, + flags: payload.flags, + allowed_mentions: payload.allowed_mentions, + useComponentsV2: payload.useComponentsV2 + }; +} + +function applyComponentsV2Defaults(payload: LoadedMessageTemplate): LoadedMessageTemplate { + const containsComponentsV2 = hasComponentsV2Components(payload.components); + const hasComponentsV2Flag = Boolean((payload.flags ?? 0) & MessageFlags.IsComponentsV2); + const resolvedUseComponentsV2 = payload.useComponentsV2 ?? (containsComponentsV2 || hasComponentsV2Flag); + + if (!resolvedUseComponentsV2) { + if (containsComponentsV2) { + throw new Error("Classic message templates cannot use Components V2 component types."); + } + + return { + ...payload, + flags: (payload.flags ?? 0) & ~MessageFlags.IsComponentsV2 + }; + } + + if (payload.content?.trim()) { + throw new Error("Components V2 message templates cannot use content. Use TextDisplay components instead."); + } + + if (payload.embeds?.length) { + throw new Error("Components V2 message templates cannot use embeds."); + } + + return { + ...payload, + flags: (payload.flags ?? 0) | MessageFlags.IsComponentsV2 + }; +} + +function sanitizeMessageTemplate(payload: LoadedMessageTemplate): LoadedMessageTemplate { + const usesV2 = usesComponentsV2(payload); + const nextPayload: LoadedMessageTemplate = {}; + + if (payload.allowed_mentions) { + nextPayload.allowed_mentions = payload.allowed_mentions; + } + + const sanitizedComponents = stripTemplateSlots(payload.components); + + if (sanitizedComponents?.length) { + nextPayload.components = sanitizedComponents; + } + + if (typeof payload.flags === "number") { + nextPayload.flags = payload.flags; + } + + if (!usesV2 && payload.content?.trim()) { + nextPayload.content = payload.content; + } + + if (!usesV2 && payload.embeds?.length) { + nextPayload.embeds = payload.embeds; + } + + return nextPayload; +} + +function hasComponentsV2Components(components: APIMessageTopLevelComponent[] | undefined) { + return components?.some((component) => COMPONENTS_V2_TYPES.has(component.type)) ?? false; +} + +function injectManyIntoSlots( + components: APIMessageTopLevelComponent[], + injectedComponents: APIMessageTopLevelComponent[], + slot: string +): { + replaced: boolean; + value: APIMessageTopLevelComponent[]; +} { + let replaced = false; + + const visit = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.flatMap((entry) => { + const resolvedEntry = visit(entry); + return Array.isArray(resolvedEntry) ? resolvedEntry : [resolvedEntry]; + }); + } + + if (isTemplateSlot(value)) { + if (value.slot !== slot || value.slot_kind !== TEMPLATE_SLOT_KIND_MANY) { + return value; + } + + replaced = true; + return structuredClone(injectedComponents); + } + + if (value && typeof value === "object") { + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry)])); + } + + return value; + }; + + return { + replaced, + value: visit(components) as APIMessageTopLevelComponent[] + }; +} + +function appendComponentsToFirstContainer( + components: APIMessageTopLevelComponent[], + appendedComponents: APIMessageTopLevelComponent[] +): { + replaced: boolean; + value: APIMessageTopLevelComponent[]; +} { + let replaced = false; + + const visit = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map((entry) => visit(entry)); + } + + if (!value || typeof value !== "object") { + return value; + } + + if (!replaced && "type" in value && value.type === ComponentType.Container && "components" in value && Array.isArray(value.components)) { + replaced = true; + return { + ...value, + components: [...value.components, ...structuredClone(appendedComponents)] + }; + } + + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry)])); + }; + + return { + replaced, + value: visit(components) as APIMessageTopLevelComponent[] + }; +} + +function stripTemplateSlots(components: APIMessageTopLevelComponent[] | undefined) { + if (!components?.length) { + return components; + } + + const visit = (value: unknown): unknown => { + if (isTemplateSlot(value)) { + return undefined; + } + + if (Array.isArray(value)) { + return value.flatMap((entry) => { + const resolvedEntry = visit(entry); + return resolvedEntry === undefined ? [] : [resolvedEntry]; + }); + } + + if (value && typeof value === "object") { + const nextValue = Object.fromEntries( + Object.entries(value).map(([key, entry]) => [key, visit(entry)]).filter(([, entry]) => entry !== undefined) + ); + + return nextValue; + } + + return value; + }; + + return visit(components) as APIMessageTopLevelComponent[]; +} + +function isTemplateSlot(value: unknown): value is { + slot: string; + slot_kind?: string; +} { + return Boolean( + value && + typeof value === "object" && + "slot" in value && + typeof value.slot === "string" && + (!("type" in value) || value.type === TEMPLATE_SLOT_TYPE) + ); +} + +function renderDeep(value: unknown, tokens: Record): unknown { + if (typeof value === "string") { + return renderTemplate(value, tokens); + } + + if (Array.isArray(value)) { + return value.map((entry) => renderDeep(entry, tokens)); + } + + if (value && typeof value === "object") { + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, renderDeep(entry, tokens)])); + } + + return value; +} diff --git a/src/features/tickets/panel-sync.ts b/src/features/tickets/panel-sync.ts new file mode 100644 index 00000000..389de7d2 --- /dev/null +++ b/src/features/tickets/panel-sync.ts @@ -0,0 +1,358 @@ +import type { + APIActionRowComponent, + APIButtonComponentWithCustomId, + APIMessageComponentInteraction, + APIMessageTopLevelComponent, + APIStringSelectComponent +} from "@discordjs/core"; +import { ComponentType, MessageFlags } from "@discordjs/core"; +import { eq } from "drizzle-orm"; +import { createCustomId } from "@/core/custom-id"; +import { reply } from "@/core/respond"; +import type { BotApp, ComponentExecutionContext } from "@/core/types"; +import { panelMessagesTable } from "@/db/schema"; +import { + getPanel, + getPanelTicketTypeKeys, + getTicketType, + userCanAccessTicketType, + validatePanelConfig +} from "@/features/tickets/config-access"; +import { appendMessageText, finalizeMessageTemplate, loadMessageTemplate } from "@/features/tickets/messages"; +import { continueTicketOpen } from "@/features/tickets/ticket-workflow"; +import type { ButtonPanelEntryConfig, PanelConfig, PanelOpenerConfig } from "@/features/tickets/types"; +import { chunk, getMemberRoleIds, mapButtonStyle, toPartialEmoji } from "@/features/tickets/utils"; + +export async function syncTicketPanels(app: BotApp) { + for (const [panelKey, panel] of Object.entries(app.config.panels)) { + try { + await syncSinglePanel(app, panelKey, panel); + } catch (error) { + app.logger.error(`Failed to sync ticket panel "${panelKey}".`, error); + } + } +} + +export async function handleOpenPanelSelector(context: ComponentExecutionContext, interaction: APIMessageComponentInteraction) { + const panelKey = context.route.state[0]; + + if (!panelKey) { + throw new Error("Missing panel key."); + } + + const panel = getPanel(context.app, panelKey); + + if (panel.opener.type !== "button-select") { + throw new Error(`Panel "${panelKey}" is not configured for button-select mode.`); + } + + const options = getVisibleTicketOptions(context.app, panel, interaction); + + if (options.length === 0) { + await reply(context.app, interaction, { + content: "You do not have access to any ticket types on this panel.", + flags: MessageFlags.Ephemeral + }); + return; + } + + await reply(context.app, interaction, { + flags: MessageFlags.Ephemeral, + components: [createSelectRow(panelKey, panel.opener, options)] + }); +} + +export async function handlePanelButtons(context: ComponentExecutionContext, interaction: APIMessageComponentInteraction) { + const ticketTypeKey = context.route.state[1]; + + if (!ticketTypeKey) { + throw new Error("Missing ticket type key."); + } + + await continueTicketOpen(context.app, interaction, { panelKey: context.route.state[0], ticketTypeKey }); +} + +export async function handlePanelSelect(context: ComponentExecutionContext, interaction: APIMessageComponentInteraction) { + const panelKey = context.route.state[0]; + + if (!panelKey) { + throw new Error("Missing panel key."); + } + + const panel = getPanel(context.app, panelKey); + const values = "values" in interaction.data ? interaction.data.values : []; + const ticketTypeKey = values[0]; + + if (!ticketTypeKey) { + await reply(context.app, interaction, { + content: "Please select a ticket type.", + flags: MessageFlags.Ephemeral + }); + return; + } + + const allowedTicketTypes = new Set(getPanelTicketTypeKeys(panel)); + + if (!allowedTicketTypes.has(ticketTypeKey)) { + await reply(context.app, interaction, { + content: "That ticket type is not available from this panel.", + flags: MessageFlags.Ephemeral + }); + return; + } + + await continueTicketOpen(context.app, interaction, { panelKey, ticketTypeKey }); +} + +async function syncSinglePanel(app: BotApp, panelKey: string, panel: PanelConfig) { + validatePanelConfig(app, panelKey, panel); + + const existingRows = await app.db.select().from(panelMessagesTable).where(eq(panelMessagesTable.panelKey, panelKey)).limit(1); + const existing = existingRows[0]; + const body = await buildPanelMessage(app, panelKey, panel); + const shouldReuseStoredMessage = existing && existing.channelId === panel.channelId; + + if (shouldReuseStoredMessage) { + // Reuse the tracked panel message when possible so admins can restart the bot + // without accumulating duplicate panel posts. + const existingMessage = await app.client.api.channels.getMessage(existing.channelId, existing.messageId).catch(() => null); + + if (existingMessage) { + if (shouldRecreateForComponentsV2(existingMessage, body)) { + const recreatedMessage = await recreatePanelMessage(app, panel, existingMessage.id, body); + await persistPanelMessage(app, panelKey, panel.channelId, recreatedMessage.id); + return; + } + + await app.client.api.channels.editMessage(panel.channelId, existing.messageId, body); + await persistPanelMessage(app, panelKey, panel.channelId, existing.messageId); + return; + } + } + + const createdMessage = await app.client.api.channels.createMessage(panel.channelId, body); + await persistPanelMessage(app, panelKey, panel.channelId, createdMessage.id); +} + +async function recreatePanelMessage( + app: BotApp, + panel: PanelConfig, + previousMessageId: string, + body: Awaited> +) { + const createdMessage = await app.client.api.channels.createMessage(panel.channelId, body); + await app.client.api.channels.deleteMessage(panel.channelId, previousMessageId).catch(() => undefined); + return createdMessage; +} + +async function buildPanelMessage(app: BotApp, panelKey: string, panel: PanelConfig) { + const messageTemplate = await loadMessageTemplate(panel.message); + const withConfiguredText = appendMessageText(messageTemplate, panel.content); + const body = placePanelOpener(withConfiguredText, buildPanelComponents(app, panelKey, panel)); + + return finalizeMessageTemplate({ + ...body, + allowed_mentions: { + ...(body.allowed_mentions ?? {}), + parse: [] + } + }); +} + +function placePanelOpener(payload: Awaited>, openerComponents: APIMessageTopLevelComponent[]) { + if (!openerComponents.length) { + return payload; + } + + let replacedSlot = false; + let appendedToContainer = false; + + const visit = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.flatMap((entry) => { + if (isPanelOpenerSlot(entry)) { + replacedSlot = true; + return structuredClone(openerComponents); + } + + const nextEntry = visit(entry); + return Array.isArray(nextEntry) ? nextEntry : [nextEntry]; + }); + } + + if (!value || typeof value !== "object") { + return value; + } + + if ( + !replacedSlot && + !appendedToContainer && + "type" in value && + value.type === ComponentType.Container && + "components" in value && + Array.isArray(value.components) + ) { + appendedToContainer = true; + return { + ...value, + components: [...value.components, ...structuredClone(openerComponents)] + }; + } + + return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry)])); + }; + + return { + ...payload, + components: visit(payload.components ?? []) as APIMessageTopLevelComponent[] + }; +} + +function isPanelOpenerSlot(value: unknown): value is { + slot: string; +} { + return Boolean(value && typeof value === "object" && "slot" in value && value.slot === "panel-opener"); +} + +function buildPanelComponents(app: BotApp, panelKey: string, panel: PanelConfig): APIMessageTopLevelComponent[] { + switch (panel.opener.type) { + case "inline-select": + return [createSelectRow(panelKey, panel.opener, buildSelectOptions(app, panel.opener.ticketTypes))]; + case "button-select": + return [createButtonRow(panelKey, panel.opener)]; + case "buttons": + return chunk(panel.opener.buttons, 5).map((buttonGroup) => createButtonsRow(app, panelKey, buttonGroup)); + } +} + +function createSelectRow( + panelKey: string, + opener: Extract, + options: APIStringSelectComponent["options"] +): APIActionRowComponent { + return { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.StringSelect, + custom_id: createCustomId("tickets", "panel-select", panelKey), + placeholder: opener.placeholder ?? "Select a ticket type", + min_values: 1, + max_values: 1, + options + } + ] + }; +} + +function createButtonRow( + panelKey: string, + opener: Extract +): APIActionRowComponent { + return { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + custom_id: createCustomId("tickets", "open-select", panelKey), + label: opener.label, + style: mapButtonStyle(opener.style), + disabled: opener.disabled, + emoji: toPartialEmoji(opener.emoji) + } + ] + }; +} + +function createButtonsRow( + app: BotApp, + panelKey: string, + entries: ButtonPanelEntryConfig[] +): APIActionRowComponent { + return { + type: ComponentType.ActionRow, + components: entries.map((entry) => ({ + type: ComponentType.Button, + custom_id: createCustomId("tickets", "open-type", panelKey, entry.ticketType), + label: entry.label ?? getTicketType(app, entry.ticketType).name, + style: mapButtonStyle(entry.style ?? "secondary"), + disabled: entry.disabled, + emoji: toPartialEmoji(entry.emoji) + })) + }; +} + +async function persistPanelMessage(app: BotApp, panelKey: string, channelId: string, messageId: string) { + await app.db + .insert(panelMessagesTable) + .values({ + panelKey, + channelId, + messageId, + updatedAt: Date.now() + }) + .onConflictDoUpdate({ + target: panelMessagesTable.panelKey, + set: { + channelId, + messageId, + updatedAt: Date.now() + } + }); +} + +function getVisibleTicketOptions(app: BotApp, panel: PanelConfig, interaction: APIMessageComponentInteraction) { + const roleIds = getMemberRoleIds(interaction); + + return buildSelectOptions( + app, + getPanelTicketTypeKeys(panel).filter((ticketTypeKey) => + userCanAccessTicketType(app, getTicketType(app, ticketTypeKey), roleIds) + ) + ); +} + +function buildSelectOptions(app: BotApp, ticketTypeKeys: string[]) { + return ticketTypeKeys.map((ticketTypeKey) => { + const ticketType = getTicketType(app, ticketTypeKey); + + return { + label: ticketType.name, + value: ticketTypeKey, + description: ticketType.description, + emoji: toPartialEmoji(ticketType.emoji) + }; + }); +} + +function shouldRecreateForComponentsV2( + existingMessage: { + content?: string; + embeds?: unknown[]; + flags?: number; + }, + nextBody: { + content?: string; + embeds?: unknown[]; + flags?: number; + } +) { + const existingUsesComponentsV2 = Boolean((existingMessage.flags ?? 0) & MessageFlags.IsComponentsV2); + const nextUsesComponentsV2 = Boolean((nextBody.flags ?? 0) & MessageFlags.IsComponentsV2); + + if (existingUsesComponentsV2 !== nextUsesComponentsV2) { + // Switching the tracked panel between classic payloads and Components V2 is + // safer as a recreate because Discord can keep incompatible fields around + // across PATCH requests. + return true; + } + + if (!nextUsesComponentsV2) { + return false; + } + + // Discord PATCH requests keep unspecified legacy fields on the existing + // message. Recreate tracked panel messages when moving them to Components V2 + // so old content/embeds do not survive the edit and invalidate the request. + return Boolean(existingMessage.content?.trim()) || Boolean(existingMessage.embeds?.length); +} diff --git a/src/features/tickets/participants.ts b/src/features/tickets/participants.ts new file mode 100644 index 00000000..4a061839 --- /dev/null +++ b/src/features/tickets/participants.ts @@ -0,0 +1,46 @@ +import { OverwriteType, PermissionFlagsBits } from "@discordjs/core"; +import { eq } from "drizzle-orm"; +import type { BotApp } from "@/core/types"; +import type { TicketRecord } from "@/db/schema"; +import { ticketsTable } from "@/db/schema"; +import { TICKET_ACCESS_ALLOW } from "@/features/tickets/constants"; + +export const MAX_INVITED_TICKET_USERS = 25; + +export function getInvitedUserIds(ticket: Pick) { + try { + const parsed = JSON.parse(ticket.invitedUserIds); + return normalizeInvitedUserIds(Array.isArray(parsed) ? parsed : []); + } catch { + return []; + } +} + +export function normalizeInvitedUserIds(userIds: string[]) { + return [...new Set(userIds.map((userId) => userId.trim()).filter(Boolean))]; +} + +export async function updateInvitedUserIds(app: BotApp, channelId: string, userIds: string[]) { + await app.db + .update(ticketsTable) + .set({ + invitedUserIds: JSON.stringify(normalizeInvitedUserIds(userIds)) + }) + .where(eq(ticketsTable.channelId, channelId)); +} + +export async function grantTicketParticipantAccess(app: BotApp, channelId: string, userId: string) { + await app.client.api.channels.editPermissionOverwrite(channelId, userId, { + type: OverwriteType.Member, + allow: TICKET_ACCESS_ALLOW.toString(), + deny: "0" + }); +} + +export async function revokeTicketParticipantAccess(app: BotApp, channelId: string, userId: string) { + await app.client.api.channels.editPermissionOverwrite(channelId, userId, { + type: OverwriteType.Member, + allow: "0", + deny: PermissionFlagsBits.ViewChannel.toString() + }); +} diff --git a/src/features/tickets/records.ts b/src/features/tickets/records.ts new file mode 100644 index 00000000..f686dc2b --- /dev/null +++ b/src/features/tickets/records.ts @@ -0,0 +1,44 @@ +import { eq } from "drizzle-orm"; +import type { BotApp } from "@/core/types"; +import { type TicketRecord, ticketsTable } from "@/db/schema"; +import { getTicketType } from "@/features/tickets/config-access"; + +export async function findTicketByChannel(app: BotApp, channelId: string) { + return app.db + .select() + .from(ticketsTable) + .where(eq(ticketsTable.channelId, channelId)) + .limit(1) + .then((rows) => rows[0] as TicketRecord | undefined); +} + +export async function getOpenTicketByChannel(app: BotApp, channelId: string | undefined) { + if (!channelId) { + return { + ok: false as const, + message: "This interaction was not used in a ticket channel." + }; + } + + const ticket = await findTicketByChannel(app, channelId); + + if (!ticket) { + return { + ok: false as const, + message: "This channel is not an open ticket." + }; + } + + if (ticket.closedAt) { + return { + ok: false as const, + message: "This ticket is already closed." + }; + } + + return { + ok: true as const, + ticket, + ticketType: getTicketType(app, ticket.type) + }; +} diff --git a/src/features/tickets/service.ts b/src/features/tickets/service.ts new file mode 100644 index 00000000..c2f14043 --- /dev/null +++ b/src/features/tickets/service.ts @@ -0,0 +1,2 @@ +export { handleOpenPanelSelector, handlePanelButtons, handlePanelSelect, syncTicketPanels } from "@/features/tickets/panel-sync"; +export { handleOpenFormSubmit } from "@/features/tickets/ticket-workflow"; diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts new file mode 100644 index 00000000..7fbd9db3 --- /dev/null +++ b/src/features/tickets/ticket-workflow.ts @@ -0,0 +1,362 @@ +import type { + APIActionRowComponent, + APIButtonComponentWithCustomId, + APIMessageComponentInteraction, + APIModalSubmitInteraction, + APIModalSubmitTextInputComponent +} from "@discordjs/core"; +import { ButtonStyle, ChannelType, ComponentType, MessageFlags, OverwriteType, PermissionFlagsBits, TextInputStyle } from "@discordjs/core"; +import { and, count, eq, isNull } from "drizzle-orm"; +import { createCustomId } from "@/core/custom-id"; +import { deferReply, editReply, followUp, reply, showModal, updateMessage } from "@/core/respond"; +import type { BotApp, ComponentExecutionContext } from "@/core/types"; +import { type TicketRecord, ticketsTable } from "@/db/schema"; +import { getPanel, getPanelTicketTypeKeys, getTicketType, getTicketStaffRoleIds, userCanAccessTicketType } from "@/features/tickets/config-access"; +import { DEFAULT_NO_REASON, TICKET_ACCESS_ALLOW } from "@/features/tickets/constants"; +import { appendMessageComponents, appendMessageText, finalizeMessageTemplate, hasMessageComponentCustomId, loadMessageTemplate } from "@/features/tickets/messages"; +import type { LoadedMessageTemplate, TicketOpenContext, TicketQuestionConfig, TicketRenderTokens, TicketTypeConfig } from "@/features/tickets/types"; +import { getInteractionUser, getMemberRoleIds, renderChannelName, renderTemplate } from "@/features/tickets/utils"; + +export async function handleOpenFormSubmit(context: ComponentExecutionContext, interaction: APIModalSubmitInteraction) { + const ticketTypeKey = context.route.state[0]; + + if (!ticketTypeKey) { + throw new Error("Missing ticket type key."); + } + + const ticketType = getTicketType(context.app, ticketTypeKey); + const questions = ticketType.openForm?.questions ?? []; + const answers = extractSubmittedValues(interaction); + const reason = questions.length > 0 ? formatQuestionAnswers(questions, answers) : DEFAULT_NO_REASON; + + await createTicket(context.app, interaction, ticketTypeKey, ticketType, reason); +} + +export async function continueTicketOpen(app: BotApp, interaction: APIMessageComponentInteraction, context: TicketOpenContext) { + const ticketType = getTicketType(app, context.ticketTypeKey); + const panel = context.panelKey ? getPanel(app, context.panelKey) : null; + const roleIds = getMemberRoleIds(interaction); + + if (!userCanAccessTicketType(app, ticketType, roleIds)) { + await reply(app, interaction, { + content: "You are not allowed to create that ticket type.", + flags: MessageFlags.Ephemeral + }); + return; + } + + if (panel) { + const allowedTypes = new Set(getPanelTicketTypeKeys(panel)); + + if (!allowedTypes.has(context.ticketTypeKey)) { + await reply(app, interaction, { + content: "That ticket type is not available from this panel.", + flags: MessageFlags.Ephemeral + }); + return; + } + } + + const currentOpenCount = await getUserOpenTicketCount(app, getInteractionUser(interaction).id); + + if (app.config.tickets.maxOpenPerUser > 0 && currentOpenCount >= app.config.tickets.maxOpenPerUser) { + await reply(app, interaction, { + content: `You already have the maximum number of open tickets (${app.config.tickets.maxOpenPerUser}).`, + flags: MessageFlags.Ephemeral + }); + return; + } + + if (ticketType.openForm?.questions.length) { + await showModal(app, interaction, { + custom_id: createCustomId("tickets", "submit-open-form", context.ticketTypeKey), + title: ticketType.openForm.title, + components: ticketType.openForm.questions.map((question) => ({ + type: ComponentType.ActionRow, + components: [createQuestionInput(question)] + })) + }); + return; + } + + if (interaction.data.component_type === ComponentType.StringSelect) { + // Update the open panel message so that it resets the selection of the user, letting them open another ticket later. + await updateMessage(app, interaction, {}); + await createTicket(app, interaction, context.ticketTypeKey, ticketType, DEFAULT_NO_REASON, { + responseMode: "follow-up" + }); + return; + } + + await createTicket(app, interaction, context.ticketTypeKey, ticketType, DEFAULT_NO_REASON); +} + +async function createTicket( + app: BotApp, + interaction: APIMessageComponentInteraction | APIModalSubmitInteraction, + ticketTypeKey: string, + ticketType: TicketTypeConfig, + reason: string, + options?: { + responseMode?: "deferred-reply" | "follow-up"; + } +) { + const responseMode = options?.responseMode ?? "deferred-reply"; + + if (responseMode === "deferred-reply") { + await deferReply(app, interaction, { flags: MessageFlags.Ephemeral }); + } + + const user = getInteractionUser(interaction); + const ticketNumber = (await getNextTicketNumber(app)).toString(); + const channelName = renderChannelName(ticketType.channelNameTemplate ?? app.config.tickets.channelNameTemplate, { + ticketNumber, + ticketTypeKey, + ticketTypeName: ticketType.name, + userId: user.id, + username: user.username + }); + + const channel = await app.client.api.guilds.createChannel(app.config.guildId, { + name: channelName, + type: ChannelType.GuildText, + parent_id: ticketType.categoryId, + permission_overwrites: buildTicketPermissionOverwrites(app, user.id, ticketType) + }); + + const tokens: TicketRenderTokens = { + channelId: channel.id, + createdByMention: `<@${user.id}>`, + reason, + ticketNumber, + ticketTypeKey, + ticketTypeName: ticketType.name, + userId: user.id, + username: user.username + }; + + const ticketMessage = await app.client.api.channels.createMessage( + channel.id, + await buildTicketWelcomeMessage(app, ticketType, tokens) + ); + + await app.db.insert(ticketsTable).values({ + channelId: channel.id, + creationMessageId: ticketMessage.id, + type: ticketTypeKey, + reason, + createdBy: user.id, + createdAt: Date.now(), + invitedUserIds: "[]" + }); + + const successMessage = { + content: `Your ticket has been created: <#${channel.id}>`, + flags: MessageFlags.Ephemeral + }; + + if (responseMode === "follow-up") { + // The initial interaction response was already consumed by updateMessage(). + await followUp(app, interaction, successMessage); + return; + } + + await editReply(app, interaction, successMessage); +} + +export async function buildTicketWelcomeMessage( + app: BotApp, + ticketType: TicketTypeConfig, + tokens: TicketRenderTokens, + options?: { + disableActions?: boolean; + } +) { + const messageReference = ticketType.message ?? app.config.tickets.defaultWelcomeMessage; + const closeButtonCustomId = createCustomId("tickets", "close"); + const claimButtonCustomId = createCustomId("tickets", "claim"); + const unclaimButtonCustomId = createCustomId("tickets", "unclaim"); + const messageTemplate = messageReference ? await loadMessageTemplate(messageReference, { + ...tokens, + closeButtonCustomId + }) : {}; + const configuredContent = ticketType.welcomeContent ?? app.config.tickets.defaultWelcomeContent; + const roleMentions = app.config.tickets.mentionRoleIds.map((roleId) => `<@&${roleId}>`); + const runtimeText = [configuredContent ? renderTemplate(configuredContent, tokens) : undefined, ...roleMentions] + .filter((part): part is string => Boolean(part?.trim())) + .join("\n") + .trim(); + const withRuntimeText = appendMessageText(messageTemplate, runtimeText); + const buttons = buildTicketActionButtons(app, withRuntimeText, { + closeButtonCustomId, + claimButtonCustomId, + unclaimButtonCustomId, + claimedBy: tokens.claimerId, + disableActions: options?.disableActions ?? false + }); + const body = appendMessageComponents(withRuntimeText, buttons, "actions"); + + return finalizeMessageTemplate({ + ...body, + allowed_mentions: { + parse: [], + users: [tokens.userId], + roles: app.config.tickets.mentionRoleIds + } + }); +} + +export async function syncTicketWelcomeMessage(app: BotApp, ticket: TicketRecord, ticketType = getTicketType(app, ticket.type)) { + const creator = await app.client.api.users.get(ticket.createdBy).catch(() => null); + const tokens: TicketRenderTokens = { + channelId: ticket.channelId, + claimStatus: formatClaimStatus(ticket.claimedBy), + claimerId: ticket.claimedBy ?? undefined, + claimerMention: ticket.claimedBy ? `<@${ticket.claimedBy}>` : undefined, + createdByMention: `<@${ticket.createdBy}>`, + reason: ticket.reason ?? DEFAULT_NO_REASON, + ticketNumber: ticket.id.toString(), + ticketTypeKey: ticket.type, + ticketTypeName: ticketType.name, + userId: ticket.createdBy, + username: creator?.username ?? ticket.createdBy + }; + const message = await buildTicketWelcomeMessage(app, ticketType, tokens); + + await app.client.api.channels.editMessage(ticket.channelId, ticket.creationMessageId, message); +} + +function buildTicketPermissionOverwrites(app: BotApp, userId: string, ticketType: TicketTypeConfig) { + const staffRoleIds = new Set(getTicketStaffRoleIds(app, ticketType)); + + return [ + { + id: app.config.guildId, + type: OverwriteType.Role, + deny: PermissionFlagsBits.ViewChannel.toString() + }, + { + id: userId, + type: OverwriteType.Member, + allow: TICKET_ACCESS_ALLOW.toString() + }, + ...Array.from(staffRoleIds).map((roleId) => ({ + id: roleId, + type: OverwriteType.Role, + allow: TICKET_ACCESS_ALLOW.toString() + })) + ]; +} + +function buildTicketActionButtons( + app: BotApp, + payload: LoadedMessageTemplate, + options: { + closeButtonCustomId: string; + claimButtonCustomId: string; + unclaimButtonCustomId: string; + claimedBy?: string; + disableActions: boolean; + } +) { + const buttons: APIButtonComponentWithCustomId[] = []; + + if (app.config.tickets.close.showCloseButton && !hasMessageComponentCustomId(payload, options.closeButtonCustomId)) { + buttons.push({ + type: ComponentType.Button, + custom_id: options.closeButtonCustomId, + label: "Close Ticket", + style: ButtonStyle.Danger, + disabled: options.disableActions + }); + } + + if (app.config.tickets.claims.enabled && app.config.tickets.claims.showButtons) { + const button = options.claimedBy + ? ({ + type: ComponentType.Button, + custom_id: options.unclaimButtonCustomId, + label: "Unclaim Ticket", + style: ButtonStyle.Secondary, + disabled: options.disableActions + } satisfies APIButtonComponentWithCustomId) + : ({ + type: ComponentType.Button, + custom_id: options.claimButtonCustomId, + label: "Claim Ticket", + style: ButtonStyle.Primary, + disabled: options.disableActions + } satisfies APIButtonComponentWithCustomId); + + if (!hasMessageComponentCustomId(payload, button.custom_id)) { + buttons.push(button); + } + } + + if (!buttons.length) { + return undefined; + } + + return [ + { + type: ComponentType.ActionRow, + components: buttons + } satisfies APIActionRowComponent + ]; +} + +function createQuestionInput(question: TicketQuestionConfig) { + return { + type: ComponentType.TextInput, + custom_id: question.key, + label: question.label, + style: question.style === "paragraph" ? TextInputStyle.Paragraph : TextInputStyle.Short, + placeholder: question.placeholder, + required: question.required ?? true, + min_length: question.minLength, + max_length: question.maxLength + }; +} + +async function getUserOpenTicketCount(app: BotApp, userId: string) { + const rows = await app.db + .select({ count: count() }) + .from(ticketsTable) + .where(and(eq(ticketsTable.createdBy, userId), isNull(ticketsTable.closedAt))); + + return Number(rows[0]?.count ?? 0); +} + +async function getNextTicketNumber(app: BotApp) { + const rows = await app.db.select({ count: count() }).from(ticketsTable); + return Number(rows[0]?.count ?? 0) + 1; +} + +function extractSubmittedValues(interaction: APIModalSubmitInteraction) { + const values = new Map(); + + for (const component of interaction.data.components) { + if (!("components" in component)) { + continue; + } + + for (const child of component.components) { + if (child.type !== ComponentType.TextInput) { + continue; + } + + values.set(child.custom_id, (child as APIModalSubmitTextInputComponent).value); + } + } + + return values; +} + +function formatQuestionAnswers(questions: TicketQuestionConfig[], answers: Map) { + const lines = questions.map((question) => `${question.label}: ${answers.get(question.key)?.trim() || DEFAULT_NO_REASON}`); + return lines.join("\n"); +} + +function formatClaimStatus(claimedBy: string | null) { + return claimedBy ? `Claimed by <@${claimedBy}>` : "Unclaimed"; +} diff --git a/src/features/tickets/transcripts.ts b/src/features/tickets/transcripts.ts new file mode 100644 index 00000000..d37c146c --- /dev/null +++ b/src/features/tickets/transcripts.ts @@ -0,0 +1,181 @@ +import type { APIMessage } from "@discordjs/core"; +import { TicketPmUploadClient } from "@ticketpm/core"; +import { buildEnrichedDiscordApiTranscriptData } from "@ticketpm/discord-api"; +import { eq } from "drizzle-orm"; +import type { BotApp } from "@/core/types"; +import { ticketsTable } from "@/db/schema"; + +const TRANSCRIPT_BASE_URL = "https://api.ticket.pm/v2"; +const TRANSCRIPT_VIEW_BASE_URL = "https://ticket.pm/"; +const TRANSCRIPT_TIMEOUT_MS = 15 * 60 * 1000; + +type TranscriptStatusHandler = (content: string) => Promise | void; + +export async function startTranscriptJob( + app: BotApp, + ticketChannelId: string, + options?: { + onStatus?: TranscriptStatusHandler; + } +) { + const promise = createTranscript(app, ticketChannelId, options?.onStatus) + .then(async (transcriptUrl) => { + if (transcriptUrl) { + await app.db.update(ticketsTable).set({ transcriptUrl }).where(eq(ticketsTable.channelId, ticketChannelId)); + } + + return transcriptUrl; + }) + .catch((error) => { + app.logger.warn(`Failed to create transcript for ticket channel ${ticketChannelId}.`, error); + return null; + }); + + return { + promise, + // Do not let transcript upload block the rest of the close flow forever. + // If ticket.pm has not finished within 15 minutes, closing continues. + waitForResult: () => waitWithTimeout(promise, TRANSCRIPT_TIMEOUT_MS) + }; +} + +async function createTranscript(app: BotApp, channelId: string, onStatus?: TranscriptStatusHandler) { + await reportStatus(onStatus, "Collecting ticket messages..."); + + const [channel, guild, messages] = await Promise.all([ + app.client.api.channels.get(channelId), + app.client.api.guilds.get(app.config.guildId).catch(() => null), + fetchAllMessages(app, channelId) + ]); + + await reportStatus(onStatus, "Creating transcript..."); + + const draftTranscript = await buildEnrichedDiscordApiTranscriptData({ + messages, + channelId, + guildId: app.config.guildId, + guild: guild + ? { + id: guild.id, + name: guild.name, + icon: guild.icon, + approximate_member_count: guild.approximate_member_count ?? undefined, + owner_id: guild.owner_id, + vanity_url_code: guild.vanity_url_code ?? null + } + : undefined, + enricher: { + fetchUser: async (userId) => await app.client.api.users.get(userId).catch(() => null), + fetchChannel: async (targetChannelId) => { + const targetChannel = await app.client.api.channels.get(targetChannelId).catch(() => null); + + if (!targetChannel) { + return null; + } + + return { + id: targetChannel.id, + type: targetChannel.type, + name: "name" in targetChannel && typeof targetChannel.name === "string" ? targetChannel.name : undefined, + parent_id: + "parent_id" in targetChannel && typeof targetChannel.parent_id === "string" ? targetChannel.parent_id : undefined + }; + }, + fetchGuildMember: async (guildId, userId) => await app.client.api.guilds.getMember(guildId, userId).catch(() => null), + fetchGuildRoles: async (guildId) => await app.client.api.guilds.getRoles(guildId).catch(() => []), + fetchPollAnswerVoters: async ({ channelId: pollChannelId, messageId, answerId }) => { + const result = await app.client.api.poll.getAnswerVoters(pollChannelId, messageId, answerId).catch(() => null); + + if (!result || !("users" in result) || !Array.isArray(result.users)) { + return []; + } + + return result.users; + } + }, + baseContext: { + channel_id: channelId, + channels: { + [channelId]: { + name: "name" in channel && typeof channel.name === "string" ? channel.name : channelId + } + } + } + }); + + await reportStatus(onStatus, "Uploading transcript..."); + + const uploadClient = new TicketPmUploadClient({ + baseUrl: TRANSCRIPT_BASE_URL + }); + const result = await uploadClient.uploadDraftTranscript(draftTranscript, { + avatarProgress: createProgressHandler("Uploading avatars...", onStatus), + mediaProgress: createProgressHandler("Uploading attachments...", onStatus) + }); + + return `${TRANSCRIPT_VIEW_BASE_URL}${result.id}`; +} + +async function fetchAllMessages(app: BotApp, channelId: string) { + const messages: APIMessage[] = []; + let before: string | undefined; + + while (true) { + const batch = await app.client.api.channels.getMessages(channelId, { + limit: 100, + before + }); + + if (batch.length === 0) { + break; + } + + messages.push(...batch); + + if (batch.length < 100) { + break; + } + + before = batch[batch.length - 1]?.id; + } + + // Discord returns channel history newest-first. Reverse it once so the + // transcript is generated in the same chronological order users saw it. + return messages.reverse(); +} + +function createProgressHandler(label: string, onStatus?: TranscriptStatusHandler) { + let lastBucket = -1; + + return (completed: number, total: number) => { + if (!onStatus || total <= 0) { + return; + } + + const bucket = total <= 10 ? completed : Math.floor((completed / total) * 10); + + if (completed !== total && bucket === lastBucket) { + return; + } + + lastBucket = bucket; + void onStatus(`${label} (${completed}/${total})`); + }; +} + +async function reportStatus(onStatus: TranscriptStatusHandler | undefined, content: string) { + if (!onStatus) { + return; + } + + await onStatus(content); +} + +async function waitWithTimeout(promise: Promise, timeoutMs: number) { + return await Promise.race([ + promise, + new Promise((resolve) => { + setTimeout(() => resolve(null), timeoutMs); + }) + ]); +} diff --git a/src/features/tickets/types.ts b/src/features/tickets/types.ts new file mode 100644 index 00000000..32995f26 --- /dev/null +++ b/src/features/tickets/types.ts @@ -0,0 +1,41 @@ +import type { APIAllowedMentions, APIEmbed, APIMessageTopLevelComponent } from "@discordjs/core"; +import type { VersionedConfig } from "@/config/index"; + +export type CurrentConfig = VersionedConfig<"0.0.1">; +export type TicketTypeConfig = CurrentConfig["ticketTypes"][string]; +export type TicketQuestionConfig = NonNullable["questions"][number]; +export type PanelConfig = CurrentConfig["panels"][string]; +export type PanelOpenerConfig = PanelConfig["opener"]; +export type ButtonPanelEntryConfig = Extract["buttons"][number]; +export type ButtonStyleName = NonNullable["style"]>; +export type TicketClaimsConfig = CurrentConfig["tickets"]["claims"]; +export type TicketClaimMode = TicketClaimsConfig["mode"]; + +export interface LoadedMessageTemplate { + allowed_mentions?: APIAllowedMentions; + content?: string; + embeds?: APIEmbed[]; + components?: APIMessageTopLevelComponent[]; + flags?: number; + useComponentsV2?: boolean; +} + +export interface TicketOpenContext { + ticketTypeKey: string; + panelKey?: string; +} + +export interface TicketRenderTokens { + [key: string]: string | undefined; + channelId?: string; + claimStatus?: string; + claimerId?: string; + claimerMention?: string; + createdByMention?: string; + reason: string; + ticketNumber: string; + ticketTypeKey: string; + ticketTypeName: string; + userId: string; + username: string; +} diff --git a/src/features/tickets/utils.ts b/src/features/tickets/utils.ts new file mode 100644 index 00000000..4bac87dd --- /dev/null +++ b/src/features/tickets/utils.ts @@ -0,0 +1,87 @@ +import type { APIUser } from "@discordjs/core"; +import { ButtonStyle } from "@discordjs/core"; +import type { ButtonStyleName } from "@/features/tickets/types"; + +export function renderTemplate(template: string, tokens: Record) { + return template.replaceAll(/\{([a-zA-Z0-9_]+)\}/g, (_, key: string) => tokens[key] ?? ""); +} + +export function renderChannelName(template: string, tokens: Record) { + return sanitizeChannelName(renderTemplate(template, tokens)); +} + +export function sanitizeChannelName(value: string) { + const cleaned = value + .toLowerCase() + .replaceAll(/\s+/g, "-") + .replaceAll(/[^a-z0-9-_]/g, "-") + .replaceAll(/-+/g, "-") + .replaceAll(/^-|-$/g, ""); + + return cleaned.slice(0, 100) || "ticket"; +} + +export function mapButtonStyle(style?: ButtonStyleName) { + switch (style) { + case "secondary": + return ButtonStyle.Secondary; + case "success": + return ButtonStyle.Success; + case "danger": + return ButtonStyle.Danger; + default: + return ButtonStyle.Primary; + } +} + +export function toPartialEmoji(emoji?: string) { + if (!emoji) { + return undefined; + } + + const discordEmojiMatch = emoji.match(/^$/); + + if (discordEmojiMatch) { + return { + id: discordEmojiMatch[1] + }; + } + + if (/^\d+$/.test(emoji)) { + return { + id: emoji + }; + } + + return { + name: emoji + }; +} + +export function chunk(values: TValue[], size: number) { + const chunks: TValue[][] = []; + + for (let index = 0; index < values.length; index += size) { + chunks.push(values.slice(index, index + size)); + } + + return chunks; +} + +export function isBlockedByRoles(blockedRoleIds: string[], roleIds: string[]) { + return blockedRoleIds.some((roleId) => roleIds.includes(roleId)); +} + +export function getInteractionUser(interaction: { member?: { user?: APIUser } | null; user?: APIUser | null }) { + const user = interaction.member?.user ?? interaction.user; + + if (!user) { + throw new Error("Missing interaction user."); + } + + return user; +} + +export function getMemberRoleIds(interaction: { member?: { roles?: string[] } | null }) { + return Array.isArray(interaction.member?.roles) ? interaction.member.roles : []; +} From b40985ba36544365e70c062fe5fe0561ad0b1ca6 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:59:30 +0200 Subject: [PATCH 09/67] chore: format --- src/features/tickets/claim-workflow.ts | 16 +++++---- src/features/tickets/close-workflow.ts | 38 ++++++++++++++------- src/features/tickets/messages.ts | 22 ++++++++++--- src/features/tickets/panel-sync.ts | 5 ++- src/features/tickets/ticket-workflow.ts | 44 ++++++++++++++++++++----- 5 files changed, 93 insertions(+), 32 deletions(-) diff --git a/src/features/tickets/claim-workflow.ts b/src/features/tickets/claim-workflow.ts index 67ca23ca..4c8dccfe 100644 --- a/src/features/tickets/claim-workflow.ts +++ b/src/features/tickets/claim-workflow.ts @@ -11,11 +11,17 @@ import { getInteractionUser, getMemberRoleIds, renderChannelName } from "@/featu type ClaimInteraction = APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction; -export async function executeClaimCommand(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction) { +export async function executeClaimCommand( + context: CommandExecutionContext, + interaction: APIChatInputApplicationCommandInteraction +) { await claimTicket(context.app, interaction); } -export async function executeUnclaimCommand(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction) { +export async function executeUnclaimCommand( + context: CommandExecutionContext, + interaction: APIChatInputApplicationCommandInteraction +) { await unclaimTicket(context.app, interaction); } @@ -70,11 +76,7 @@ async function claimTicket(app: BotApp, interaction: ClaimInteraction) { await updateClaimedTicketPresentation(app, nextTicketState, ticketType.name, actor.username); - await syncTicketWelcomeMessage( - app, - nextTicketState, - ticketType - ); + await syncTicketWelcomeMessage(app, nextTicketState, ticketType); await replyWithContent( app, diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index 8de07881..e1f2d143 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -12,7 +12,12 @@ import { editReply, reply, showModal } from "@/core/respond"; import type { BotApp, CommandExecutionContext, ComponentExecutionContext } from "@/core/types"; import { ticketsTable } from "@/db/schema"; import { getTicketType, hasTicketStaffAccess } from "@/features/tickets/config-access"; -import { appendMessageButton, finalizeMessageTemplate, hasMessageComponentCustomId, loadMessageTemplate } from "@/features/tickets/messages"; +import { + appendMessageButton, + finalizeMessageTemplate, + hasMessageComponentCustomId, + loadMessageTemplate +} from "@/features/tickets/messages"; import { getInvitedUserIds, revokeTicketParticipantAccess } from "@/features/tickets/participants"; import { findTicketByChannel, getOpenTicketByChannel } from "@/features/tickets/records"; import { startTranscriptJob } from "@/features/tickets/transcripts"; @@ -21,7 +26,10 @@ import { getInteractionUser, getMemberRoleIds } from "@/features/tickets/utils"; const DEFAULT_CLOSE_DM_MESSAGE = "tickets/ticket-closed-dm"; const DEFAULT_CLOSE_CHANNEL_MESSAGE = "tickets/ticket-closed"; -export async function executeCloseCommand(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction) { +export async function executeCloseCommand( + context: CommandExecutionContext, + interaction: APIChatInputApplicationCommandInteraction +) { await beginCloseFlow(context.app, interaction); } @@ -69,7 +77,13 @@ async function beginCloseFlow( app: BotApp, interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction ) { - const closable = await getClosableTicket(app, interaction.channel_id, getMemberRoleIds(interaction), getInteractionUser(interaction).id, true); + const closable = await getClosableTicket( + app, + interaction.channel_id, + getMemberRoleIds(interaction), + getInteractionUser(interaction).id, + true + ); if (!closable.ok) { await reply(app, interaction, { @@ -404,15 +418,15 @@ async function buildCloseChannelMessage( return finalizeMessageTemplate( appendMessageButton( - messageTemplate, - !hasMessageComponentCustomId(messageTemplate, deleteButtonCustomId) - ? ({ - type: ComponentType.Button, - custom_id: deleteButtonCustomId, - label: "Delete Ticket", - style: ButtonStyle.Danger - } satisfies APIButtonComponentWithCustomId) - : undefined + messageTemplate, + !hasMessageComponentCustomId(messageTemplate, deleteButtonCustomId) + ? ({ + type: ComponentType.Button, + custom_id: deleteButtonCustomId, + label: "Delete Ticket", + style: ButtonStyle.Danger + } satisfies APIButtonComponentWithCustomId) + : undefined ) ); } diff --git a/src/features/tickets/messages.ts b/src/features/tickets/messages.ts index b3af9df1..9b81dd33 100644 --- a/src/features/tickets/messages.ts +++ b/src/features/tickets/messages.ts @@ -69,7 +69,10 @@ export function appendMessageText(payload: LoadedMessageTemplate, text: string | return { ...payload, - content: [payload.content, normalizedText].filter((part): part is string => Boolean(part?.trim())).join("\n").trim() + content: [payload.content, normalizedText] + .filter((part): part is string => Boolean(part?.trim())) + .join("\n") + .trim() }; } @@ -168,7 +171,10 @@ export function hasMessageComponentCustomId(payload: LoadedMessageTemplate, cust } function usesComponentsV2(payload: LoadedMessageTemplate) { - return payload.useComponentsV2 ?? (Boolean((payload.flags ?? 0) & MessageFlags.IsComponentsV2) || hasComponentsV2Components(payload.components)); + return ( + payload.useComponentsV2 ?? + (Boolean((payload.flags ?? 0) & MessageFlags.IsComponentsV2) || hasComponentsV2Components(payload.components)) + ); } async function resolveMessageTemplatePath(reference: string) { @@ -343,7 +349,13 @@ function appendComponentsToFirstContainer( return value; } - if (!replaced && "type" in value && value.type === ComponentType.Container && "components" in value && Array.isArray(value.components)) { + if ( + !replaced && + "type" in value && + value.type === ComponentType.Container && + "components" in value && + Array.isArray(value.components) + ) { replaced = true; return { ...value, @@ -379,7 +391,9 @@ function stripTemplateSlots(components: APIMessageTopLevelComponent[] | undefine if (value && typeof value === "object") { const nextValue = Object.fromEntries( - Object.entries(value).map(([key, entry]) => [key, visit(entry)]).filter(([, entry]) => entry !== undefined) + Object.entries(value) + .map(([key, entry]) => [key, visit(entry)]) + .filter(([, entry]) => entry !== undefined) ); return nextValue; diff --git a/src/features/tickets/panel-sync.ts b/src/features/tickets/panel-sync.ts index 389de7d2..99c93623 100644 --- a/src/features/tickets/panel-sync.ts +++ b/src/features/tickets/panel-sync.ts @@ -159,7 +159,10 @@ async function buildPanelMessage(app: BotApp, panelKey: string, panel: PanelConf }); } -function placePanelOpener(payload: Awaited>, openerComponents: APIMessageTopLevelComponent[]) { +function placePanelOpener( + payload: Awaited>, + openerComponents: APIMessageTopLevelComponent[] +) { if (!openerComponents.length) { return payload; } diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index 7fbd9db3..24276ebe 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -5,16 +5,42 @@ import type { APIModalSubmitInteraction, APIModalSubmitTextInputComponent } from "@discordjs/core"; -import { ButtonStyle, ChannelType, ComponentType, MessageFlags, OverwriteType, PermissionFlagsBits, TextInputStyle } from "@discordjs/core"; +import { + ButtonStyle, + ChannelType, + ComponentType, + MessageFlags, + OverwriteType, + PermissionFlagsBits, + TextInputStyle +} from "@discordjs/core"; import { and, count, eq, isNull } from "drizzle-orm"; import { createCustomId } from "@/core/custom-id"; import { deferReply, editReply, followUp, reply, showModal, updateMessage } from "@/core/respond"; import type { BotApp, ComponentExecutionContext } from "@/core/types"; import { type TicketRecord, ticketsTable } from "@/db/schema"; -import { getPanel, getPanelTicketTypeKeys, getTicketType, getTicketStaffRoleIds, userCanAccessTicketType } from "@/features/tickets/config-access"; +import { + getPanel, + getPanelTicketTypeKeys, + getTicketType, + getTicketStaffRoleIds, + userCanAccessTicketType +} from "@/features/tickets/config-access"; import { DEFAULT_NO_REASON, TICKET_ACCESS_ALLOW } from "@/features/tickets/constants"; -import { appendMessageComponents, appendMessageText, finalizeMessageTemplate, hasMessageComponentCustomId, loadMessageTemplate } from "@/features/tickets/messages"; -import type { LoadedMessageTemplate, TicketOpenContext, TicketQuestionConfig, TicketRenderTokens, TicketTypeConfig } from "@/features/tickets/types"; +import { + appendMessageComponents, + appendMessageText, + finalizeMessageTemplate, + hasMessageComponentCustomId, + loadMessageTemplate +} from "@/features/tickets/messages"; +import type { + LoadedMessageTemplate, + TicketOpenContext, + TicketQuestionConfig, + TicketRenderTokens, + TicketTypeConfig +} from "@/features/tickets/types"; import { getInteractionUser, getMemberRoleIds, renderChannelName, renderTemplate } from "@/features/tickets/utils"; export async function handleOpenFormSubmit(context: ComponentExecutionContext, interaction: APIModalSubmitInteraction) { @@ -176,10 +202,12 @@ export async function buildTicketWelcomeMessage( const closeButtonCustomId = createCustomId("tickets", "close"); const claimButtonCustomId = createCustomId("tickets", "claim"); const unclaimButtonCustomId = createCustomId("tickets", "unclaim"); - const messageTemplate = messageReference ? await loadMessageTemplate(messageReference, { - ...tokens, - closeButtonCustomId - }) : {}; + const messageTemplate = messageReference + ? await loadMessageTemplate(messageReference, { + ...tokens, + closeButtonCustomId + }) + : {}; const configuredContent = ticketType.welcomeContent ?? app.config.tickets.defaultWelcomeContent; const roleMentions = app.config.tickets.mentionRoleIds.map((roleId) => `<@&${roleId}>`); const runtimeText = [configuredContent ? renderTemplate(configuredContent, tokens) : undefined, ...roleMentions] From 343b0d563f3ed76a119b98fdd19b4474aa49a58e Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:14:35 +0200 Subject: [PATCH 10/67] feat(core): auto-deploy commands on ready event * Export deployApplicationCommands from deploy-commands file. * Call deployment function directly in the ready event. * Remove old deploy:commands script from package.json. --- package.json | 1 - src/deploy-commands.ts | 54 +++++++++++++++++++++++++++++------------- src/events/ready.ts | 8 +++++++ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 8659f16c..d25af756 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "format": "biome format", "format:fix": "biome format --write", "drizzle:push": "bunx drizzle-kit push", - "deploy:commands": "bun src/deploy-commands.ts", "start": "bun src/index.ts" }, "dependencies": { diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index 0d6990be..a614da9b 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -1,15 +1,36 @@ import { API } from "@discordjs/core"; import { REST } from "@discordjs/rest"; +import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord-api-types/v10"; import { config } from "dotenv"; import { discoverCommands, discoverEvents, discoverFeatures } from "@/core/discovery"; -import { createLogger } from "@/core/logger"; +import { createLogger, type Logger } from "@/core/logger"; import { createHandlerRegistry } from "@/core/registry"; import botConfig from "../config/config.ts"; config({ path: "./config/.env" }); const logger = createLogger("deploy"); -const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); -const api = new API(rest); + +export async function deployApplicationCommands(options: { + applicationCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[]; + clientId: string; + guildId?: string; + logger: Logger; + token: string; +}) { + const rest = new REST({ version: "10" }).setToken(options.token); + const api = new API(rest); + + if (options.guildId) { + await api.applicationCommands.bulkOverwriteGuildCommands(options.clientId, options.guildId, options.applicationCommands); + + options.logger.info(`Deployed ${options.applicationCommands.length} guild commands to ${options.guildId}.`); + return; + } + + await api.applicationCommands.bulkOverwriteGlobalCommands(options.clientId, options.applicationCommands); + + options.logger.info(`Deployed ${options.applicationCommands.length} global commands.`); +} async function deployCommands() { const [commands, events, features] = await Promise.all([ @@ -19,19 +40,18 @@ async function deployCommands() { ]); const registry = createHandlerRegistry({ commands, features, events, logger }); - if (botConfig.guildId) { - await api.applicationCommands.bulkOverwriteGuildCommands(botConfig.clientId, botConfig.guildId, registry.applicationCommands); - - logger.info(`Deployed ${registry.applicationCommands.length} guild commands to ${botConfig.guildId}.`); - return; - } - - await api.applicationCommands.bulkOverwriteGlobalCommands(botConfig.clientId, registry.applicationCommands); - - logger.info(`Deployed ${registry.applicationCommands.length} global commands.`); + await deployApplicationCommands({ + applicationCommands: registry.applicationCommands, + clientId: botConfig.clientId, + guildId: botConfig.guildId, + logger, + token: process.env.DISCORD_TOKEN + }); } -deployCommands().catch((error) => { - logger.error("Failed to deploy commands", error); - process.exit(1); -}); +if (import.meta.main) { + deployCommands().catch((error) => { + logger.error("Failed to deploy commands", error); + process.exit(1); + }); +} diff --git a/src/events/ready.ts b/src/events/ready.ts index aec6a364..d3081b27 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,5 +1,6 @@ import { GatewayDispatchEvents, type GatewayReadyDispatchData, type ToEventProps } from "@discordjs/core"; import { defineEvent } from "@/core/defineEvent"; +import { deployApplicationCommands } from "@/deploy-commands"; import { syncTicketPanels } from "@/features/tickets/service"; const readyEvent = defineEvent<[ToEventProps]>({ @@ -8,6 +9,13 @@ const readyEvent = defineEvent<[ToEventProps]>({ async execute(app, event) { app.logger.info(`Connected as ${event.data.user.username}.`); + await deployApplicationCommands({ + applicationCommands: app.registry.applicationCommands, + clientId: app.config.clientId, + guildId: app.config.guildId, + logger: app.logger, + token: process.env.DISCORD_TOKEN + }); await syncTicketPanels(app); } }); From b4624fe82a66022eaa10f4ed449d4563cafed8c9 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:14:58 +0200 Subject: [PATCH 11/67] feat(tickets): pin welcome message on ticket creation * Pin the main welcome message immediately upon channel creation. * Attempt cleanup of the system pin notification message. --- src/features/tickets/ticket-workflow.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index 24276ebe..961f18d4 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -22,8 +22,8 @@ import { type TicketRecord, ticketsTable } from "@/db/schema"; import { getPanel, getPanelTicketTypeKeys, - getTicketType, getTicketStaffRoleIds, + getTicketType, userCanAccessTicketType } from "@/features/tickets/config-access"; import { DEFAULT_NO_REASON, TICKET_ACCESS_ALLOW } from "@/features/tickets/constants"; @@ -165,6 +165,7 @@ async function createTicket( channel.id, await buildTicketWelcomeMessage(app, ticketType, tokens) ); + await pinTicketWelcomeMessage(app, channel.id, ticketMessage.id); await app.db.insert(ticketsTable).values({ channelId: channel.id, @@ -190,6 +191,23 @@ async function createTicket( await editReply(app, interaction, successMessage); } +async function pinTicketWelcomeMessage(app: BotApp, channelId: string, messageId: string) { + try { + await app.client.api.channels.pinMessage(channelId, messageId); + + const messages = await app.client.api.channels.getMessages(channelId, { limit: 5 }).catch(() => []); + const pinNotice = messages.find((message) => message.id !== messageId && message.type === 6); + + if (!pinNotice) { + return; + } + + await app.client.api.channels.deleteMessage(channelId, pinNotice.id).catch(() => null); + } catch (error) { + app.logger.warn(`Failed to pin the welcome message in ticket channel ${channelId}.`, error); + } +} + export async function buildTicketWelcomeMessage( app: BotApp, ticketType: TicketTypeConfig, From d3561bdcff49d4036f43bf0c5453e9a2e45abe6d Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:14:59 +0200 Subject: [PATCH 12/67] feat(tickets): support authenticated transcripts and styling options * Add TICKETPM_PASSKEY environment variable for authorized uploads. * Introduce UUID style configuration for transcripts. * Relax ProcessEnv types to handle undefined values correctly. --- config/.env.example | 9 ++ config/config.example.ts | 212 ++++++++++++++++++++++++++++ src/config/index.ts | 2 + src/features/tickets/transcripts.ts | 12 +- src/types/process.d.ts | 3 +- 5 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 config/.env.example create mode 100644 config/config.example.ts diff --git a/config/.env.example b/config/.env.example new file mode 100644 index 00000000..d68bbbcd --- /dev/null +++ b/config/.env.example @@ -0,0 +1,9 @@ +# Discord bot token from the Developer Portal. +DISCORD_TOKEN=replace-me-with-your-bot-token + +# Database connection string used by libsql/drizzle. +# Local SQLite example: +DB_FILE_NAME=file:.data/ticket-bot.db + +# Optional ticket.pm passkey used to upload transcripts while authenticated. Get yours on https://ticket.pm/app/account +TICKETPM_PASSKEY= diff --git a/config/config.example.ts b/config/config.example.ts new file mode 100644 index 00000000..fb4f10f9 --- /dev/null +++ b/config/config.example.ts @@ -0,0 +1,212 @@ +import { defineConfig } from "@/config/index.ts"; + +export default defineConfig("0.0.1", { + // Your Discord application (bot) client ID. + clientId: "123456789012345678", + // The guild where the bot is installed and where commands should be deployed. + guildId: "123456789012345678", + lang: "en", + // Transcript ID style used by ticket.pm uploads. + // "uuid" matches the current default. "emoji" keeps the older style. + uuidType: "uuid", + + tickets: { + // Fallback channel name used when a ticket type does not override it. + // Available parameters here: + // {ticketNumber} {ticketTypeKey} {ticketTypeName} {userId} {username} + channelNameTemplate: "{ticketNumber}-ticket-{username}", + // How many open tickets a single user may have at once. Use 0 for unlimited. + maxOpenPerUser: 1, + // Global staff roles that can see and manage tickets. + staffRoleIds: ["111111111111111111"], + // Roles that are blocked from opening any ticket type by default. + blockedRoleIds: ["222222222222222222"], + // Roles mentioned in the welcome message when a ticket is opened. + mentionRoleIds: ["333333333333333333"], + // Message template path inside the messages directory. + defaultWelcomeMessage: "tickets/ticket-opened", + // Optional plain text appended to the welcome message template. + defaultWelcomeContent: "A staff member will be with you shortly. Please explain your issue clearly.", + + claims: { + enabled: true, + // soft: claiming is optional + // strict: tickets must be claimed before they can be closed + // display-only: claimed state is shown, but close rules do not change + mode: "soft", + // Adds claim and unclaim buttons to the welcome message. + showButtons: true, + // Lets the current claimer release the ticket. + allowUnclaim: true, + // Optional rename applied after a successful claim. + // Available parameters here: + // {claimerId} {claimerUsername} {createdById} {createdByUsername} + // {ticketNumber} {ticketTypeKey} {ticketTypeName} + nameWhenClaimed: "{ticketNumber}-claimed-{claimerUsername}", + // Optional category move applied after a successful claim. + // Leave blank to keep the ticket in its original category. + categoryWhenClaimed: "444444444444444444", + // disabled: nobody can take an existing claim + // staff: any configured staff member can take over + // roles: only roles listed in takeoverRoleIds can take over + takeoverMode: "roles", + takeoverRoleIds: ["555555555555555555"] + }, + close: { + // If true, only staff can close tickets. + staffOnly: true, + // Send the opener a DM when the ticket closes. + dmUserOnClose: true, + // Ask the closer for a reason before closing. + askForReason: true, + // Adds a close button to the ticket welcome message. + showCloseButton: true, + // Delete the channel after close instead of keeping it around for review. + deleteChannelOnClose: false, + // Generate a transcript through the configured transcript provider. + createTranscript: true, + // Optional category for closed tickets when the channel is not deleted. + // Leave blank to keep the ticket where it is. + closeTicketCategoryId: "666666666666666666", + // Message template path used for the DM sent on close. + dmMessage: "tickets/ticket-closed-dm", + // Message template path posted in the closed ticket channel. + channelMessage: "tickets/ticket-closed" + } + }, + + ticketTypes: { + general: { + name: "General Support", + description: "General help and account questions.", + // You can use a unicode emoji, a custom emoji string like <:name:id>, or just an emoji ID. + emoji: "<:ticket:171717171717171717>", + categoryId: "777777777777777777", + // Optional per-type channel name override. + channelNameTemplate: "{ticketNumber}-general-{username}", + // Optional per-type welcome message template override. + message: "tickets/ticket-opened", + // Optional plain text appended after the message template. + welcomeContent: "Tell us what you need help with and include screenshots if they matter.", + // Optional per-type block list. + blockedRoleIds: ["888888888888888888"], + // Optional per-type staff roles in addition to global staff roles. + staffRoleIds: ["999999999999999999"] + }, + billing: { + name: "Billing", + description: "Payments, invoices, and subscription issues.", + emoji: "<:billing:181818181818181818>", + categoryId: "101010101010101010", + welcomeContent: "Please include invoice numbers, order IDs, or the last payment date if you have them.", + staffRoleIds: ["121212121212121212"], + openForm: { + title: "Billing Ticket", + questions: [ + { + key: "orderNumber", + label: "Order number", + placeholder: "Example: INV-12345", + style: "short", + required: false, + maxLength: 100 + }, + { + key: "issue", + label: "What is the billing issue?", + placeholder: "Explain the problem clearly", + style: "paragraph", + required: true, + minLength: 10, + maxLength: 1000 + } + ] + } + }, + report: { + name: "Report", + description: "Report a player, member, or rule violation.", + emoji: "<:report:191919191919191919>", + categoryId: "131313131313131313", + openForm: { + title: "Report Details", + questions: [ + { + key: "user", + label: "Who are you reporting?", + placeholder: "Username or user ID", + style: "short", + required: true, + maxLength: 100 + }, + { + key: "summary", + label: "What happened?", + placeholder: "Write a clear summary of the issue", + style: "paragraph", + required: true, + minLength: 20, + maxLength: 1000 + } + ] + } + } + }, + + panels: { + supportSelect: { + channelId: "141414141414141414", + // Message template path inside the messages directory. + message: "tickets/open-panel", + // Optional text posted alongside the panel template. + content: "Choose the ticket type that fits your issue best.", + opener: { + type: "inline-select", + ticketTypes: ["general", "billing", "report"], + placeholder: "Open a ticket" + } + }, + supportButtonSelect: { + channelId: "151515151515151515", + message: "tickets/open-panel", + content: "Click the button, then choose the matching ticket type.", + opener: { + type: "button-select", + ticketTypes: ["general", "billing"], + label: "Open Support Ticket", + emoji: "<:open_ticket:202020202020202020>", + style: "primary", + placeholder: "Choose a ticket type", + disabled: false + } + }, + quickButtons: { + channelId: "161616161616161616", + message: "tickets/open-panel", + content: "Fast one-click ticket buttons for the most common flows.", + opener: { + type: "buttons", + buttons: [ + { + ticketType: "general", + label: "General Help", + emoji: "<:ticket:171717171717171717>", + style: "primary" + }, + { + ticketType: "billing", + label: "Billing Help", + emoji: "<:billing:181818181818181818>", + style: "secondary" + }, + { + ticketType: "report", + label: "Report User", + emoji: "<:report:191919191919191919>", + style: "danger" + } + ] + } + } + } +}); diff --git a/src/config/index.ts b/src/config/index.ts index adf5c53a..53f810fa 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -5,6 +5,8 @@ interface ConfigV0_0_1 { guildId: string; /** The lang of the bot */ lang: "en"; + /** Controls the transcript ID style when uploading to ticket.pm */ + uuidType?: "uuid" | "emoji"; tickets: { channelNameTemplate: string; maxOpenPerUser: number; diff --git a/src/features/tickets/transcripts.ts b/src/features/tickets/transcripts.ts index d37c146c..3791e3ad 100644 --- a/src/features/tickets/transcripts.ts +++ b/src/features/tickets/transcripts.ts @@ -1,4 +1,3 @@ -import type { APIMessage } from "@discordjs/core"; import { TicketPmUploadClient } from "@ticketpm/core"; import { buildEnrichedDiscordApiTranscriptData } from "@ticketpm/discord-api"; import { eq } from "drizzle-orm"; @@ -10,6 +9,7 @@ const TRANSCRIPT_VIEW_BASE_URL = "https://ticket.pm/"; const TRANSCRIPT_TIMEOUT_MS = 15 * 60 * 1000; type TranscriptStatusHandler = (content: string) => Promise | void; +type TranscriptSourceMessage = Parameters[0]["messages"][number]; export async function startTranscriptJob( app: BotApp, @@ -106,9 +106,11 @@ async function createTranscript(app: BotApp, channelId: string, onStatus?: Trans await reportStatus(onStatus, "Uploading transcript..."); const uploadClient = new TicketPmUploadClient({ - baseUrl: TRANSCRIPT_BASE_URL + baseUrl: TRANSCRIPT_BASE_URL, + token: process.env.TICKETPM_PASSKEY }); const result = await uploadClient.uploadDraftTranscript(draftTranscript, { + uuidStyleIds: app.config.uuidType !== "emoji", avatarProgress: createProgressHandler("Uploading avatars...", onStatus), mediaProgress: createProgressHandler("Uploading attachments...", onStatus) }); @@ -117,7 +119,7 @@ async function createTranscript(app: BotApp, channelId: string, onStatus?: Trans } async function fetchAllMessages(app: BotApp, channelId: string) { - const messages: APIMessage[] = []; + const messages: TranscriptSourceMessage[] = []; let before: string | undefined; while (true) { @@ -130,7 +132,9 @@ async function fetchAllMessages(app: BotApp, channelId: string) { break; } - messages.push(...batch); + // discord.js/core and @ticketpm/discord-api may resolve different + // discord-api-types package instances, so keep the cast local here. + messages.push(...(batch as TranscriptSourceMessage[])); if (batch.length < 100) { break; diff --git a/src/types/process.d.ts b/src/types/process.d.ts index b4a0eb92..a3ecef3e 100644 --- a/src/types/process.d.ts +++ b/src/types/process.d.ts @@ -2,9 +2,10 @@ export {}; declare global { namespace NodeJS { - interface ProcessEnv extends Record { + interface ProcessEnv extends Record { DB_FILE_NAME: string; DISCORD_TOKEN: string; + TICKETPM_PASSKEY?: string; } } } From c41714b3e450b4521194b4678cb6c0b2e7e2b09b Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:14:59 +0200 Subject: [PATCH 13/67] refactor(commands): rename massadd to mass_add and sort imports * Change massadd command identifier and file structure to mass_add. * Standardize import sorting across command files. --- src/features/commands/add/command.ts | 2 +- src/features/commands/{massadd => mass_add}/command.ts | 4 ++-- src/features/commands/remove/command.ts | 2 +- src/features/commands/rename/command.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/features/commands/{massadd => mass_add}/command.ts (99%) diff --git a/src/features/commands/add/command.ts b/src/features/commands/add/command.ts index b178a774..e59f84d3 100644 --- a/src/features/commands/add/command.ts +++ b/src/features/commands/add/command.ts @@ -1,8 +1,8 @@ import { MessageFlags } from "@discordjs/core"; import { ApplicationCommandOptionType } from "discord-api-types/v10"; -import { getUserOption } from "@/features/commands/shared/options"; import { defineCommand } from "@/core/defineCommand"; import { reply } from "@/core/respond"; +import { getUserOption } from "@/features/commands/shared/options"; import { getInvitedUserIds, grantTicketParticipantAccess, diff --git a/src/features/commands/massadd/command.ts b/src/features/commands/mass_add/command.ts similarity index 99% rename from src/features/commands/massadd/command.ts rename to src/features/commands/mass_add/command.ts index a920b90b..2969c556 100644 --- a/src/features/commands/massadd/command.ts +++ b/src/features/commands/mass_add/command.ts @@ -1,8 +1,8 @@ import { MessageFlags } from "@discordjs/core"; import { ApplicationCommandOptionType } from "discord-api-types/v10"; -import { getStringOption } from "@/features/commands/shared/options"; import { defineCommand } from "@/core/defineCommand"; import { reply } from "@/core/respond"; +import { getStringOption } from "@/features/commands/shared/options"; import { getInvitedUserIds, grantTicketParticipantAccess, @@ -13,7 +13,7 @@ import { getOpenTicketByChannel } from "@/features/tickets/records"; export default defineCommand({ data: { - name: "massadd", + name: "mass_add", description: "Add multiple users to the current ticket", options: [ { diff --git a/src/features/commands/remove/command.ts b/src/features/commands/remove/command.ts index 8e9b04e1..9d18e343 100644 --- a/src/features/commands/remove/command.ts +++ b/src/features/commands/remove/command.ts @@ -1,11 +1,11 @@ import type { APIChatInputApplicationCommandInteraction, APIMessageComponentInteraction } from "@discordjs/core"; import { ComponentType, MessageFlags } from "@discordjs/core"; import { ApplicationCommandOptionType } from "discord-api-types/v10"; -import { getUserOption } from "@/features/commands/shared/options"; import { createCustomId } from "@/core/custom-id"; import { defineCommand } from "@/core/defineCommand"; import { reply, updateMessage } from "@/core/respond"; import type { ComponentExecutionContext } from "@/core/types"; +import { getUserOption } from "@/features/commands/shared/options"; import { getInvitedUserIds, revokeTicketParticipantAccess, updateInvitedUserIds } from "@/features/tickets/participants"; import { getOpenTicketByChannel } from "@/features/tickets/records"; diff --git a/src/features/commands/rename/command.ts b/src/features/commands/rename/command.ts index ce69dcc4..ae43755d 100644 --- a/src/features/commands/rename/command.ts +++ b/src/features/commands/rename/command.ts @@ -1,8 +1,8 @@ import { MessageFlags } from "@discordjs/core"; import { ApplicationCommandOptionType } from "discord-api-types/v10"; -import { getStringOption } from "@/features/commands/shared/options"; import { defineCommand } from "@/core/defineCommand"; import { reply } from "@/core/respond"; +import { getStringOption } from "@/features/commands/shared/options"; import { hasTicketStaffAccess } from "@/features/tickets/config-access"; import { getOpenTicketByChannel } from "@/features/tickets/records"; import { getMemberRoleIds, sanitizeChannelName } from "@/features/tickets/utils"; From 320ff637e953e78ab9b9a6721bfb09b3336b3c78 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:15:33 +0200 Subject: [PATCH 14/67] fix(config): add quiet option to dotenv configuration --- src/deploy-commands.ts | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index a614da9b..ecf6e800 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -7,7 +7,7 @@ import { createLogger, type Logger } from "@/core/logger"; import { createHandlerRegistry } from "@/core/registry"; import botConfig from "../config/config.ts"; -config({ path: "./config/.env" }); +config({ path: "./config/.env", quiet: true }); const logger = createLogger("deploy"); export async function deployApplicationCommands(options: { diff --git a/src/index.ts b/src/index.ts index 926aa0b7..e34a2ad0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { config } from "dotenv"; import { createBotApp } from "@/app"; -config({ path: "./config/.env" }); +config({ path: "./config/.env", quiet: true }); async function main() { const { start, stop } = await createBotApp(); From 10fbeb71686c67b24778a9df5e57fcd3e727d605 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:29:53 +0200 Subject: [PATCH 15/67] feat(core): configure bot presence and update template docs * Introduce status block in configuration schema. * Apply presence during ready event with a refresh interval. * Document new available tokens for ticket channel and message templates. --- config/config.example.ts | 28 ++++++++++++++++++++++ src/config/index.ts | 7 ++++++ src/events/ready.ts | 52 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/config/config.example.ts b/config/config.example.ts index fb4f10f9..ac887ba1 100644 --- a/config/config.example.ts +++ b/config/config.example.ts @@ -9,6 +9,18 @@ export default defineConfig("0.0.1", { // Transcript ID style used by ticket.pm uploads. // "uuid" matches the current default. "emoji" keeps the older style. uuidType: "uuid", + status: { + // Set to false to leave the bot presence untouched. + enabled: true, + // Activity text shown in the member list. + text: "github.com/Sayrix", + // PLAYING, STREAMING, LISTENING, WATCHING, CUSTOM, COMPETING + type: "WATCHING", + // Only used for STREAMING. + url: "https://twitch.tv/example", + // online, idle, dnd, invisible + status: "online" + }, tickets: { // Fallback channel name used when a ticket type does not override it. @@ -26,6 +38,10 @@ export default defineConfig("0.0.1", { // Message template path inside the messages directory. defaultWelcomeMessage: "tickets/ticket-opened", // Optional plain text appended to the welcome message template. + // Available parameters here: + // {channelId} {claimStatus} {claimerId} {claimerMention} {createdByMention} + // {reason} {reason1} {reason2} ... {reasonN} + // {ticketNumber} {ticketTypeKey} {ticketTypeName} {userId} {username} defaultWelcomeContent: "A staff member will be with you shortly. Please explain your issue clearly.", claims: { @@ -83,10 +99,16 @@ export default defineConfig("0.0.1", { emoji: "<:ticket:171717171717171717>", categoryId: "777777777777777777", // Optional per-type channel name override. + // Available parameters here: + // {ticketNumber} {ticketTypeKey} {ticketTypeName} {userId} {username} channelNameTemplate: "{ticketNumber}-general-{username}", // Optional per-type welcome message template override. message: "tickets/ticket-opened", // Optional plain text appended after the message template. + // Available parameters here: + // {channelId} {claimStatus} {claimerId} {claimerMention} {createdByMention} + // {reason} {reason1} {reason2} ... {reasonN} + // {ticketNumber} {ticketTypeKey} {ticketTypeName} {userId} {username} welcomeContent: "Tell us what you need help with and include screenshots if they matter.", // Optional per-type block list. blockedRoleIds: ["888888888888888888"], @@ -98,6 +120,10 @@ export default defineConfig("0.0.1", { description: "Payments, invoices, and subscription issues.", emoji: "<:billing:181818181818181818>", categoryId: "101010101010101010", + // Available parameters here: + // {channelId} {claimStatus} {claimerId} {claimerMention} {createdByMention} + // {reason} {reason1} {reason2} ... {reasonN} + // {ticketNumber} {ticketTypeKey} {ticketTypeName} {userId} {username} welcomeContent: "Please include invoice numbers, order IDs, or the last payment date if you have them.", staffRoleIds: ["121212121212121212"], openForm: { @@ -128,6 +154,8 @@ export default defineConfig("0.0.1", { description: "Report a player, member, or rule violation.", emoji: "<:report:191919191919191919>", categoryId: "131313131313131313", + welcomeContent: + "Details: {reason1}\n\nAdditional info: {reason2}\n\nPlease provide any evidence you have and our staff will review it as soon as possible.", openForm: { title: "Report Details", questions: [ diff --git a/src/config/index.ts b/src/config/index.ts index 53f810fa..03287841 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -7,6 +7,13 @@ interface ConfigV0_0_1 { lang: "en"; /** Controls the transcript ID style when uploading to ticket.pm */ uuidType?: "uuid" | "emoji"; + status?: { + enabled: boolean; + text?: string; + type?: "PLAYING" | "STREAMING" | "LISTENING" | "WATCHING" | "CUSTOM" | "COMPETING"; + url?: string; + status: "online" | "idle" | "dnd" | "invisible"; + }; tickets: { channelNameTemplate: string; maxOpenPerUser: number; diff --git a/src/events/ready.ts b/src/events/ready.ts index d3081b27..2cb083a8 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,8 +1,26 @@ import { GatewayDispatchEvents, type GatewayReadyDispatchData, type ToEventProps } from "@discordjs/core"; +import { ActivityType, type GatewayPresenceUpdateData, PresenceUpdateStatus } from "discord-api-types/v10"; import { defineEvent } from "@/core/defineEvent"; +import type { BotApp } from "@/core/types"; import { deployApplicationCommands } from "@/deploy-commands"; import { syncTicketPanels } from "@/features/tickets/service"; +const PRESENCE_REFRESH_INTERVAL_MS = 900_000; +const ACTIVITY_TYPES = { + COMPETING: ActivityType.Competing, + CUSTOM: ActivityType.Custom, + LISTENING: ActivityType.Listening, + PLAYING: ActivityType.Playing, + STREAMING: ActivityType.Streaming, + WATCHING: ActivityType.Watching +} as const; +const PRESENCE_STATUSES = { + dnd: PresenceUpdateStatus.DoNotDisturb, + idle: PresenceUpdateStatus.Idle, + invisible: PresenceUpdateStatus.Invisible, + online: PresenceUpdateStatus.Online +} as const; + const readyEvent = defineEvent<[ToEventProps]>({ name: GatewayDispatchEvents.Ready, once: true, @@ -17,7 +35,41 @@ const readyEvent = defineEvent<[ToEventProps]>({ token: process.env.DISCORD_TOKEN }); await syncTicketPanels(app); + await applyConfiguredPresence(app); + setInterval(() => { + void applyConfiguredPresence(app); + }, PRESENCE_REFRESH_INTERVAL_MS); } }); export default readyEvent; + +async function applyConfiguredPresence(app: BotApp) { + const configuredStatus = app.config.status; + + if (!configuredStatus?.enabled) { + return; + } + + const activities = + configuredStatus.type && configuredStatus.text + ? [ + { + name: configuredStatus.text, + type: ACTIVITY_TYPES[configuredStatus.type], + url: configuredStatus.type === "STREAMING" && configuredStatus.url?.trim() ? configuredStatus.url : undefined + } + ] + : []; + const presence: GatewayPresenceUpdateData = { + activities, + afk: false, + since: null, + status: PRESENCE_STATUSES[configuredStatus.status] + }; + const shardCount = await app.client.gateway.getShardCount(); + + for (let shardId = 0; shardId < shardCount; shardId += 1) { + await app.client.updatePresence(shardId, presence); + } +} From dc277dabfd7d05256643e3126a91cf676ee1c47f Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:30:12 +0200 Subject: [PATCH 16/67] feat(tickets): support dynamic form answers in welcome templates * Introduce tokens for individual open form answers ({reasonN}). * Add runtime text slots for modular template rendering. * Refactor staff mentions to utilize specific message slots. * Serialize ticket open reasons securely with backward compatibility. --- messages/tickets/ticket-opened.ts | 5 +- src/features/tickets/messages.ts | 14 ++- src/features/tickets/ticket-workflow.ts | 146 ++++++++++++++++++++---- 3 files changed, 137 insertions(+), 28 deletions(-) diff --git a/messages/tickets/ticket-opened.ts b/messages/tickets/ticket-opened.ts index 50173979..9348faf0 100644 --- a/messages/tickets/ticket-opened.ts +++ b/messages/tickets/ticket-opened.ts @@ -1,12 +1,12 @@ import { ComponentType } from "@discordjs/core"; -import { createMessageSlot } from "@/features/tickets/messages"; +import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; import type { LoadedMessageTemplate } from "@/features/tickets/types"; const ticketOpenedMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, - content: "{createdByMention}" + content: "{createdByMention}{staffMentions}" }, { type: ComponentType.Container, @@ -24,6 +24,7 @@ const ticketOpenedMessage: LoadedMessageTemplate = { type: ComponentType.TextDisplay, content: "**Details**\n{reason}" }, + createRuntimeTextSlot(), { type: ComponentType.TextDisplay, content: "**Claim Status**\n{claimStatus}" diff --git a/src/features/tickets/messages.ts b/src/features/tickets/messages.ts index 9b81dd33..d8e2a1a4 100644 --- a/src/features/tickets/messages.ts +++ b/src/features/tickets/messages.ts @@ -31,6 +31,10 @@ export function createPanelOpenerSlot() { return createMessageSlot("panel-opener"); } +export function createRuntimeTextSlot() { + return createMessageSlot("runtime-text"); +} + export async function loadMessageTemplate( reference: string, tokens?: Record @@ -49,7 +53,13 @@ export function finalizeMessageTemplate(payload: LoadedMessageTemplate) { return sanitizeMessageTemplate(applyComponentsV2Defaults(payload)); } -export function appendMessageText(payload: LoadedMessageTemplate, text: string | undefined) { +export function appendMessageText( + payload: LoadedMessageTemplate, + text: string | undefined, + options?: { + slot?: string; + } +) { const normalizedText = text?.trim(); if (!normalizedText) { @@ -64,7 +74,7 @@ export function appendMessageText(payload: LoadedMessageTemplate, text: string | type: ComponentType.TextDisplay, content: normalizedText } - ]); + ], options?.slot); } return { diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index 961f18d4..0bfb9087 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -43,6 +43,11 @@ import type { } from "@/features/tickets/types"; import { getInteractionUser, getMemberRoleIds, renderChannelName, renderTemplate } from "@/features/tickets/utils"; +interface TicketOpenReasonData { + answers: string[]; + combined: string; +} + export async function handleOpenFormSubmit(context: ComponentExecutionContext, interaction: APIModalSubmitInteraction) { const ticketTypeKey = context.route.state[0]; @@ -53,7 +58,7 @@ export async function handleOpenFormSubmit(context: ComponentExecutionContext, i const ticketType = getTicketType(context.app, ticketTypeKey); const questions = ticketType.openForm?.questions ?? []; const answers = extractSubmittedValues(interaction); - const reason = questions.length > 0 ? formatQuestionAnswers(questions, answers) : DEFAULT_NO_REASON; + const reason = questions.length > 0 ? createTicketOpenReason(questions, answers) : createDefaultTicketOpenReason(); await createTicket(context.app, interaction, ticketTypeKey, ticketType, reason); } @@ -108,13 +113,13 @@ export async function continueTicketOpen(app: BotApp, interaction: APIMessageCom if (interaction.data.component_type === ComponentType.StringSelect) { // Update the open panel message so that it resets the selection of the user, letting them open another ticket later. await updateMessage(app, interaction, {}); - await createTicket(app, interaction, context.ticketTypeKey, ticketType, DEFAULT_NO_REASON, { + await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason(), { responseMode: "follow-up" }); return; } - await createTicket(app, interaction, context.ticketTypeKey, ticketType, DEFAULT_NO_REASON); + await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason()); } async function createTicket( @@ -122,7 +127,7 @@ async function createTicket( interaction: APIMessageComponentInteraction | APIModalSubmitInteraction, ticketTypeKey: string, ticketType: TicketTypeConfig, - reason: string, + reason: TicketOpenReasonData, options?: { responseMode?: "deferred-reply" | "follow-up"; } @@ -149,17 +154,16 @@ async function createTicket( parent_id: ticketType.categoryId, permission_overwrites: buildTicketPermissionOverwrites(app, user.id, ticketType) }); - - const tokens: TicketRenderTokens = { + const tokens = createTicketRenderTokens({ channelId: channel.id, createdByMention: `<@${user.id}>`, - reason, + openReason: reason, ticketNumber, ticketTypeKey, ticketTypeName: ticketType.name, userId: user.id, username: user.username - }; + }); const ticketMessage = await app.client.api.channels.createMessage( channel.id, @@ -171,7 +175,7 @@ async function createTicket( channelId: channel.id, creationMessageId: ticketMessage.id, type: ticketTypeKey, - reason, + reason: serializeTicketOpenReason(reason), createdBy: user.id, createdAt: Date.now(), invitedUserIds: "[]" @@ -220,19 +224,18 @@ export async function buildTicketWelcomeMessage( const closeButtonCustomId = createCustomId("tickets", "close"); const claimButtonCustomId = createCustomId("tickets", "claim"); const unclaimButtonCustomId = createCustomId("tickets", "unclaim"); + const roleMentions = app.config.tickets.mentionRoleIds.map((roleId) => `<@&${roleId}>`); + const renderedTokens = { + ...tokens, + closeButtonCustomId, + staffMentions: roleMentions.length ? ` ${roleMentions.join(" ")}` : "" + }; const messageTemplate = messageReference - ? await loadMessageTemplate(messageReference, { - ...tokens, - closeButtonCustomId - }) + ? await loadMessageTemplate(messageReference, renderedTokens) : {}; const configuredContent = ticketType.welcomeContent ?? app.config.tickets.defaultWelcomeContent; - const roleMentions = app.config.tickets.mentionRoleIds.map((roleId) => `<@&${roleId}>`); - const runtimeText = [configuredContent ? renderTemplate(configuredContent, tokens) : undefined, ...roleMentions] - .filter((part): part is string => Boolean(part?.trim())) - .join("\n") - .trim(); - const withRuntimeText = appendMessageText(messageTemplate, runtimeText); + const runtimeText = configuredContent ? renderTemplate(configuredContent, renderedTokens) : undefined; + const withRuntimeText = appendMessageText(messageTemplate, runtimeText, { slot: "runtime-text" }); const buttons = buildTicketActionButtons(app, withRuntimeText, { closeButtonCustomId, claimButtonCustomId, @@ -254,19 +257,19 @@ export async function buildTicketWelcomeMessage( export async function syncTicketWelcomeMessage(app: BotApp, ticket: TicketRecord, ticketType = getTicketType(app, ticket.type)) { const creator = await app.client.api.users.get(ticket.createdBy).catch(() => null); - const tokens: TicketRenderTokens = { + const tokens = createTicketRenderTokens({ channelId: ticket.channelId, claimStatus: formatClaimStatus(ticket.claimedBy), claimerId: ticket.claimedBy ?? undefined, claimerMention: ticket.claimedBy ? `<@${ticket.claimedBy}>` : undefined, createdByMention: `<@${ticket.createdBy}>`, - reason: ticket.reason ?? DEFAULT_NO_REASON, + openReason: parseStoredTicketOpenReason(ticket.reason), ticketNumber: ticket.id.toString(), ticketTypeKey: ticket.type, ticketTypeName: ticketType.name, userId: ticket.createdBy, username: creator?.username ?? ticket.createdBy - }; + }); const message = await buildTicketWelcomeMessage(app, ticketType, tokens); await app.client.api.channels.editMessage(ticket.channelId, ticket.creationMessageId, message); @@ -398,11 +401,106 @@ function extractSubmittedValues(interaction: APIModalSubmitInteraction) { return values; } -function formatQuestionAnswers(questions: TicketQuestionConfig[], answers: Map) { - const lines = questions.map((question) => `${question.label}: ${answers.get(question.key)?.trim() || DEFAULT_NO_REASON}`); +function createDefaultTicketOpenReason(): TicketOpenReasonData { + return { + answers: [], + combined: DEFAULT_NO_REASON + }; +} + +function createTicketOpenReason(questions: TicketQuestionConfig[], answers: Map): TicketOpenReasonData { + const normalizedAnswers = questions.map((question) => normalizeAnswer(answers.get(question.key))); + + return { + answers: normalizedAnswers, + combined: formatQuestionAnswers(questions, normalizedAnswers) + }; +} + +function createTicketRenderTokens(input: { + channelId?: string; + claimStatus?: string; + claimerId?: string; + claimerMention?: string; + createdByMention?: string; + openReason: TicketOpenReasonData; + ticketNumber: string; + ticketTypeKey: string; + ticketTypeName: string; + userId: string; + username: string; +}) { + const tokens: TicketRenderTokens = { + channelId: input.channelId, + claimStatus: input.claimStatus ?? formatClaimStatus(input.claimerId ?? null), + claimerId: input.claimerId, + claimerMention: input.claimerMention, + createdByMention: input.createdByMention, + reason: input.openReason.combined, + ticketNumber: input.ticketNumber, + ticketTypeKey: input.ticketTypeKey, + ticketTypeName: input.ticketTypeName, + userId: input.userId, + username: input.username + }; + + for (const [index, answer] of input.openReason.answers.entries()) { + const placeholderNumber = (index + 1).toString(); + + tokens[`reason${placeholderNumber}`] = answer; + } + + return tokens; +} + +function formatQuestionAnswers(questions: TicketQuestionConfig[], answers: string[]) { + const lines = questions.map((question, index) => `${question.label}: ${answers[index] ?? DEFAULT_NO_REASON}`); return lines.join("\n"); } function formatClaimStatus(claimedBy: string | null) { return claimedBy ? `Claimed by <@${claimedBy}>` : "Unclaimed"; } + +function normalizeAnswer(answer: string | undefined) { + return answer?.trim() || DEFAULT_NO_REASON; +} + +function serializeTicketOpenReason(reason: TicketOpenReasonData) { + if (!reason.answers.length) { + return reason.combined; + } + + return JSON.stringify({ + answers: reason.answers, + combined: reason.combined, + version: 1 + }); +} + +function parseStoredTicketOpenReason(reason: string | null | undefined): TicketOpenReasonData { + if (!reason) { + return createDefaultTicketOpenReason(); + } + + try { + const parsed = JSON.parse(reason) as Partial & { version?: number }; + + if (parsed.version !== 1 || !Array.isArray(parsed.answers) || typeof parsed.combined !== "string") { + return { + answers: [], + combined: reason + }; + } + + return { + answers: parsed.answers.map((answer) => normalizeAnswer(typeof answer === "string" ? answer : undefined)), + combined: parsed.combined + }; + } catch { + return { + answers: [], + combined: reason + }; + } +} From 4828b14448f730830a76525151babaec770a0dac Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:48:58 +0200 Subject: [PATCH 17/67] Add license notice to all relevant files for Ticket-Bot --- config/config.example.ts | 30 ++++++++++++++++++++++ drizzle.config.ts | 30 ++++++++++++++++++++++ messages/tickets/open-panel.ts | 30 ++++++++++++++++++++++ messages/tickets/ticket-closed-dm.ts | 30 ++++++++++++++++++++++ messages/tickets/ticket-closed.ts | 30 ++++++++++++++++++++++ messages/tickets/ticket-opened.ts | 30 ++++++++++++++++++++++ src/app.ts | 30 ++++++++++++++++++++++ src/config/index.ts | 30 ++++++++++++++++++++++ src/core/custom-id.ts | 30 ++++++++++++++++++++++ src/core/defineCommand.ts | 30 ++++++++++++++++++++++ src/core/defineEvent.ts | 30 ++++++++++++++++++++++ src/core/defineFeature.ts | 30 ++++++++++++++++++++++ src/core/discovery.ts | 30 ++++++++++++++++++++++ src/core/logger.ts | 30 ++++++++++++++++++++++ src/core/registry.ts | 30 ++++++++++++++++++++++ src/core/respond.ts | 30 ++++++++++++++++++++++ src/core/router.ts | 30 ++++++++++++++++++++++ src/core/types.ts | 30 ++++++++++++++++++++++ src/db/schema.ts | 30 ++++++++++++++++++++++ src/deploy-commands.ts | 30 ++++++++++++++++++++++ src/events/interactionCreate.ts | 30 ++++++++++++++++++++++ src/events/ready.ts | 30 ++++++++++++++++++++++ src/features/commands/add/command.ts | 30 ++++++++++++++++++++++ src/features/commands/claim/command.ts | 30 ++++++++++++++++++++++ src/features/commands/cleardm/command.ts | 30 ++++++++++++++++++++++ src/features/commands/close/command.ts | 30 ++++++++++++++++++++++ src/features/commands/mass_add/command.ts | 30 ++++++++++++++++++++++ src/features/commands/remove/command.ts | 30 ++++++++++++++++++++++ src/features/commands/rename/command.ts | 30 ++++++++++++++++++++++ src/features/commands/shared/options.ts | 30 ++++++++++++++++++++++ src/features/commands/unclaim/command.ts | 30 ++++++++++++++++++++++ src/features/tickets/claim-workflow.ts | 30 ++++++++++++++++++++++ src/features/tickets/close-workflow.ts | 30 ++++++++++++++++++++++ src/features/tickets/config-access.ts | 30 ++++++++++++++++++++++ src/features/tickets/constants.ts | 30 ++++++++++++++++++++++ src/features/tickets/feature.ts | 30 ++++++++++++++++++++++ src/features/tickets/messages.ts | 30 ++++++++++++++++++++++ src/features/tickets/panel-sync.ts | 30 ++++++++++++++++++++++ src/features/tickets/participants.ts | 30 ++++++++++++++++++++++ src/features/tickets/records.ts | 30 ++++++++++++++++++++++ src/features/tickets/service.ts | 30 ++++++++++++++++++++++ src/features/tickets/ticket-workflow.ts | 30 ++++++++++++++++++++++ src/features/tickets/transcripts.ts | 30 ++++++++++++++++++++++ src/features/tickets/types.ts | 30 ++++++++++++++++++++++ src/features/tickets/utils.ts | 30 ++++++++++++++++++++++ src/index.ts | 30 ++++++++++++++++++++++ src/types/config.d.ts | 31 +++++++++++++++++++++++ src/types/discordjs-rest.d.ts | 30 ++++++++++++++++++++++ src/types/index.d.ts | 30 ++++++++++++++++++++++ src/types/process.d.ts | 30 ++++++++++++++++++++++ 50 files changed, 1501 insertions(+) diff --git a/config/config.example.ts b/config/config.example.ts index ac887ba1..db7483a4 100644 --- a/config/config.example.ts +++ b/config/config.example.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { defineConfig } from "@/config/index.ts"; export default defineConfig("0.0.1", { @@ -238,3 +253,18 @@ export default defineConfig("0.0.1", { } } }); + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/drizzle.config.ts b/drizzle.config.ts index 5d43f6d4..f9ee5d0c 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { config } from "dotenv"; import { defineConfig } from "drizzle-kit"; @@ -11,3 +26,18 @@ export default defineConfig({ url: process.env.DB_FILE_NAME } }); + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/open-panel.ts b/messages/tickets/open-panel.ts index 07d1b1dc..a578f09e 100644 --- a/messages/tickets/open-panel.ts +++ b/messages/tickets/open-panel.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { ComponentType } from "discord-api-types/v10"; import { createPanelOpenerSlot } from "@/features/tickets/messages"; import type { LoadedMessageTemplate } from "@/features/tickets/types"; @@ -48,3 +63,18 @@ const openPanelMessage: LoadedMessageTemplate = { }; export default openPanelMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-closed-dm.ts b/messages/tickets/ticket-closed-dm.ts index 4c2e486d..f7932f86 100644 --- a/messages/tickets/ticket-closed-dm.ts +++ b/messages/tickets/ticket-closed-dm.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { ComponentType } from "@discordjs/core"; import type { LoadedMessageTemplate } from "@/features/tickets/types"; @@ -33,3 +48,18 @@ const ticketClosedDmMessage: LoadedMessageTemplate = { }; export default ticketClosedDmMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-closed.ts b/messages/tickets/ticket-closed.ts index 09c1147e..3e0ab210 100644 --- a/messages/tickets/ticket-closed.ts +++ b/messages/tickets/ticket-closed.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { ComponentType } from "@discordjs/core"; import type { LoadedMessageTemplate } from "@/features/tickets/types"; @@ -48,3 +63,18 @@ const ticketClosedMessage: LoadedMessageTemplate = { }; export default ticketClosedMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-opened.ts b/messages/tickets/ticket-opened.ts index 9348faf0..7e197a16 100644 --- a/messages/tickets/ticket-opened.ts +++ b/messages/tickets/ticket-opened.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { ComponentType } from "@discordjs/core"; import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; import type { LoadedMessageTemplate } from "@/features/tickets/types"; @@ -36,3 +51,18 @@ const ticketOpenedMessage: LoadedMessageTemplate = { }; export default ticketOpenedMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/app.ts b/src/app.ts index 99fe2fcd..b00b3b05 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { Client, GatewayIntentBits } from "@discordjs/core"; import { REST } from "@discordjs/rest"; import { WebSocketManager } from "@discordjs/ws"; @@ -59,3 +74,18 @@ export async function createBotApp() { } }; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/config/index.ts b/src/config/index.ts index 03287841..71d4356a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + interface ConfigV0_0_1 { /** The client ID of the bot application */ clientId: string; @@ -125,3 +140,18 @@ export function defineConfig(version: V, config: Config ...config }; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/custom-id.ts b/src/core/custom-id.ts index e09629a5..b1eb76fe 100644 --- a/src/core/custom-id.ts +++ b/src/core/custom-id.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + const CUSTOM_ID_SEPARATOR = ":"; export interface ParsedCustomId { @@ -23,3 +38,18 @@ export function parseCustomId(customId: string): ParsedCustomId | null { state: rawState.map((part) => decodeURIComponent(part)) }; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/defineCommand.ts b/src/core/defineCommand.ts index 0ee50e8c..54d4ce48 100644 --- a/src/core/defineCommand.ts +++ b/src/core/defineCommand.ts @@ -1,5 +1,35 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { CommandModule } from "@/core/types"; export function defineCommand(command: TCommand) { return command; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/defineEvent.ts b/src/core/defineEvent.ts index cc27c71c..5bdf9ef4 100644 --- a/src/core/defineEvent.ts +++ b/src/core/defineEvent.ts @@ -1,5 +1,35 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { EventModule } from "@/core/types"; export function defineEvent(event: EventModule) { return event; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/defineFeature.ts b/src/core/defineFeature.ts index 2f8e2c08..650814ee 100644 --- a/src/core/defineFeature.ts +++ b/src/core/defineFeature.ts @@ -1,5 +1,35 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { FeatureModule } from "@/core/types"; export function defineFeature(feature: TFeature) { return feature; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/discovery.ts b/src/core/discovery.ts index cfb53538..5a60f4ea 100644 --- a/src/core/discovery.ts +++ b/src/core/discovery.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { readdir } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -115,3 +130,18 @@ export async function discoverEvents(logger: Logger) { "events" ); } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/logger.ts b/src/core/logger.ts index 71e469f3..840292f4 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + export interface Logger { info(message: string, ...meta: unknown[]): void; warn(message: string, ...meta: unknown[]): void; @@ -28,3 +43,18 @@ export function createLogger(scope: string): Logger { } }; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/registry.ts b/src/core/registry.ts index 6e86a99e..fcfc76ad 100644 --- a/src/core/registry.ts +++ b/src/core/registry.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { Logger } from "@/core/logger"; import type { BotApp, CommandModule, EventModule, FeatureModule, HandlerRegistry } from "@/core/types"; @@ -63,3 +78,18 @@ export function registerEvents(app: BotApp) { eventClient.on(event.name as never, listener); } } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/respond.ts b/src/core/respond.ts index 0ec1ecdd..95f6e297 100644 --- a/src/core/respond.ts +++ b/src/core/respond.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { APIApplicationCommandAutocompleteInteraction, APIChatInputApplicationCommandInteraction, @@ -48,3 +63,18 @@ export async function replyWithError(app: BotApp, interaction: ReplyableInteract flags: MessageFlags.Ephemeral }).catch(() => undefined); } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/router.ts b/src/core/router.ts index 50a241bc..4256b29b 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { APIApplicationCommandAutocompleteInteraction, APIApplicationCommandInteraction, @@ -163,3 +178,18 @@ export class InteractionRouter { await handler(context, interaction); } } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/core/types.ts b/src/core/types.ts index 454d3f73..a6121b1e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { APIApplicationCommandAutocompleteInteraction, APIApplicationCommandInteraction, @@ -79,3 +94,18 @@ export type StringSelectHandler = ( ) => Promise; export type ModalHandler = (context: ComponentExecutionContext, interaction: APIModalSubmitInteraction) => Promise; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/db/schema.ts b/src/db/schema.ts index 492fcbf9..f9d7e04a 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { int, sqliteTable, text } from "drizzle-orm/sqlite-core"; // NOTE: Columns that does not have notNull constraint ARE nullable. @@ -35,3 +50,18 @@ export const ticketsTable = sqliteTable("tickets", { }); export type TicketRecord = typeof ticketsTable.$inferSelect; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index ecf6e800..505e685a 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { API } from "@discordjs/core"; import { REST } from "@discordjs/rest"; import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord-api-types/v10"; @@ -55,3 +70,18 @@ if (import.meta.main) { process.exit(1); }); } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 84bd403a..688a5d69 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { GatewayInteractionCreateDispatchData, ToEventProps } from "@discordjs/core"; import { GatewayDispatchEvents, InteractionType } from "@discordjs/core"; import { defineEvent } from "@/core/defineEvent"; @@ -14,3 +29,18 @@ const interactionCreateEvent = defineEvent<[ToEventProps): return value; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/panel-sync.ts b/src/features/tickets/panel-sync.ts index 99c93623..35da359a 100644 --- a/src/features/tickets/panel-sync.ts +++ b/src/features/tickets/panel-sync.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { APIActionRowComponent, APIButtonComponentWithCustomId, @@ -359,3 +374,18 @@ function shouldRecreateForComponentsV2( // so old content/embeds do not survive the edit and invalidate the request. return Boolean(existingMessage.content?.trim()) || Boolean(existingMessage.embeds?.length); } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/participants.ts b/src/features/tickets/participants.ts index 4a061839..cd139b3b 100644 --- a/src/features/tickets/participants.ts +++ b/src/features/tickets/participants.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { OverwriteType, PermissionFlagsBits } from "@discordjs/core"; import { eq } from "drizzle-orm"; import type { BotApp } from "@/core/types"; @@ -44,3 +59,18 @@ export async function revokeTicketParticipantAccess(app: BotApp, channelId: stri deny: PermissionFlagsBits.ViewChannel.toString() }); } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/records.ts b/src/features/tickets/records.ts index f686dc2b..8348961d 100644 --- a/src/features/tickets/records.ts +++ b/src/features/tickets/records.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { eq } from "drizzle-orm"; import type { BotApp } from "@/core/types"; import { type TicketRecord, ticketsTable } from "@/db/schema"; @@ -42,3 +57,18 @@ export async function getOpenTicketByChannel(app: BotApp, channelId: string | un ticketType: getTicketType(app, ticket.type) }; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/service.ts b/src/features/tickets/service.ts index c2f14043..fa7b2ee0 100644 --- a/src/features/tickets/service.ts +++ b/src/features/tickets/service.ts @@ -1,2 +1,32 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + export { handleOpenPanelSelector, handlePanelButtons, handlePanelSelect, syncTicketPanels } from "@/features/tickets/panel-sync"; export { handleOpenFormSubmit } from "@/features/tickets/ticket-workflow"; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index 0bfb9087..7371e67d 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { APIActionRowComponent, APIButtonComponentWithCustomId, @@ -504,3 +519,18 @@ function parseStoredTicketOpenReason(reason: string | null | undefined): TicketO }; } } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/transcripts.ts b/src/features/tickets/transcripts.ts index 3791e3ad..3d67dbef 100644 --- a/src/features/tickets/transcripts.ts +++ b/src/features/tickets/transcripts.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { TicketPmUploadClient } from "@ticketpm/core"; import { buildEnrichedDiscordApiTranscriptData } from "@ticketpm/discord-api"; import { eq } from "drizzle-orm"; @@ -183,3 +198,18 @@ async function waitWithTimeout(promise: Promise, timeoutMs: numb }) ]); } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/types.ts b/src/features/tickets/types.ts index 32995f26..e4963de6 100644 --- a/src/features/tickets/types.ts +++ b/src/features/tickets/types.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { APIAllowedMentions, APIEmbed, APIMessageTopLevelComponent } from "@discordjs/core"; import type { VersionedConfig } from "@/config/index"; @@ -39,3 +54,18 @@ export interface TicketRenderTokens { userId: string; username: string; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/utils.ts b/src/features/tickets/utils.ts index 4bac87dd..66b8c97d 100644 --- a/src/features/tickets/utils.ts +++ b/src/features/tickets/utils.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import type { APIUser } from "@discordjs/core"; import { ButtonStyle } from "@discordjs/core"; import type { ButtonStyleName } from "@/features/tickets/types"; @@ -85,3 +100,18 @@ export function getInteractionUser(interaction: { member?: { user?: APIUser } | export function getMemberRoleIds(interaction: { member?: { roles?: string[] } | null }) { return Array.isArray(interaction.member?.roles) ? interaction.member.roles : []; } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/index.ts b/src/index.ts index e34a2ad0..6d96928b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + import { config } from "dotenv"; import { createBotApp } from "@/app"; @@ -21,3 +36,18 @@ main().catch(async (error) => { console.error("[boot] Failed to start bot", error); process.exit(1); }); + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/types/config.d.ts b/src/types/config.d.ts index e69de29b..cb81edab 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -0,0 +1,31 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + + + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/types/discordjs-rest.d.ts b/src/types/discordjs-rest.d.ts index 7203305d..fa005bea 100644 --- a/src/types/discordjs-rest.d.ts +++ b/src/types/discordjs-rest.d.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + declare module "@discordjs/rest" { export class REST { public constructor(options?: { version?: string }); @@ -5,3 +20,18 @@ declare module "@discordjs/rest" { public on(event: string, listener: (payload: unknown) => void): void; } } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/types/index.d.ts b/src/types/index.d.ts index cb0ff5c3..affa9428 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1 +1,31 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + export {}; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/types/process.d.ts b/src/types/process.d.ts index a3ecef3e..4af5de51 100644 --- a/src/types/process.d.ts +++ b/src/types/process.d.ts @@ -1,3 +1,18 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + export {}; declare global { @@ -9,3 +24,18 @@ declare global { } } } + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ From 870fe63cc8f5d14501972bd98b40abac79921a6b Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:51:03 +0200 Subject: [PATCH 18/67] feat(config): add per-type close message overrides * Add close configuration to ticket types schema * Resolve correct template files for DMs and channel closures * Pass ticketType properties down the messaging pipeline --- config/config.example.ts | 29 +++++++++++++++++++++----- src/config/index.ts | 4 ++++ src/features/tickets/close-workflow.ts | 29 +++++++++++++++++++------- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/config/config.example.ts b/config/config.example.ts index db7483a4..0901b9a4 100644 --- a/config/config.example.ts +++ b/config/config.example.ts @@ -50,7 +50,10 @@ export default defineConfig("0.0.1", { blockedRoleIds: ["222222222222222222"], // Roles mentioned in the welcome message when a ticket is opened. mentionRoleIds: ["333333333333333333"], - // Message template path inside the messages directory. + // Fallback open-ticket template path inside the messages directory. + // Create your own file under messages/ and point a ticket type at it. + // Example file: messages/tickets/ticket-opened-billing.ts + // Example config path: "tickets/ticket-opened-billing" defaultWelcomeMessage: "tickets/ticket-opened", // Optional plain text appended to the welcome message template. // Available parameters here: @@ -99,9 +102,11 @@ export default defineConfig("0.0.1", { // Optional category for closed tickets when the channel is not deleted. // Leave blank to keep the ticket where it is. closeTicketCategoryId: "666666666666666666", - // Message template path used for the DM sent on close. + // Global fallback template path for the DM sent on close. + // A ticket type can override this with ticketTypes..close.dmMessage. dmMessage: "tickets/ticket-closed-dm", - // Message template path posted in the closed ticket channel. + // Global fallback template path posted in the closed ticket channel. + // A ticket type can override this with ticketTypes..close.channelMessage. channelMessage: "tickets/ticket-closed" } }, @@ -117,7 +122,9 @@ export default defineConfig("0.0.1", { // Available parameters here: // {ticketNumber} {ticketTypeKey} {ticketTypeName} {userId} {username} channelNameTemplate: "{ticketNumber}-general-{username}", - // Optional per-type welcome message template override. + // Optional per-type open-ticket template override. + // Copy messages/tickets/ticket-opened.ts to a new file if this type + // needs its own embed/container layout, then link it here. message: "tickets/ticket-opened", // Optional plain text appended after the message template. // Available parameters here: @@ -135,11 +142,21 @@ export default defineConfig("0.0.1", { description: "Payments, invoices, and subscription issues.", emoji: "<:billing:181818181818181818>", categoryId: "101010101010101010", + // This ticket type still uses the global open-ticket template. + // If you want a custom open layout, create another file in messages/ + // and set `message` here the same way as the close overrides below. // Available parameters here: // {channelId} {claimStatus} {claimerId} {claimerMention} {createdByMention} // {reason} {reason1} {reason2} ... {reasonN} // {ticketNumber} {ticketTypeKey} {ticketTypeName} {userId} {username} welcomeContent: "Please include invoice numbers, order IDs, or the last payment date if you have them.", + // Optional per-type close templates. + // These override tickets.close.dmMessage and tickets.close.channelMessage. + // The sample files below are included in this repository. + close: { + dmMessage: "tickets/ticket-closed-dm-billing", + channelMessage: "tickets/ticket-closed-billing" + }, staffRoleIds: ["121212121212121212"], openForm: { title: "Billing Ticket", @@ -199,7 +216,9 @@ export default defineConfig("0.0.1", { panels: { supportSelect: { channelId: "141414141414141414", - // Message template path inside the messages directory. + // Each panel can use its own template file inside the messages directory. + // Example: create messages/tickets/open-panel-billing.ts and point this + // to "tickets/open-panel-billing" if you want a different panel layout. message: "tickets/open-panel", // Optional text posted alongside the panel template. content: "Choose the ticket type that fits your issue best.", diff --git a/src/config/index.ts b/src/config/index.ts index 71d4356a..e6def4d6 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -69,6 +69,10 @@ interface ConfigV0_0_1 { channelNameTemplate?: string; message?: string; welcomeContent?: string; + close?: { + channelMessage?: string; + dmMessage?: string; + }; blockedRoleIds?: string[]; staffRoleIds?: string[]; openForm?: { diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index 18e65e96..e295ad87 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -164,7 +164,7 @@ async function closeTicket( const status = createCloseStatusUpdater(app, interaction); await status.start(app.config.tickets.close.createTranscript ? "Preparing transcript..." : "Closing ticket..."); - const { ticket } = closable; + const { ticket, ticketType } = closable; const closer = getInteractionUser(interaction); const normalizedReason = normalizeCloseReason(reason); @@ -223,7 +223,7 @@ async function closeTicket( if (app.config.tickets.close.dmUserOnClose) { await status.update("Sending close confirmation..."); - await sendCloseDm(app, ticket.createdBy, closeMessageTokens); + await sendCloseDm(app, ticket.createdBy, ticketType, closeMessageTokens); } if (app.config.tickets.close.deleteChannelOnClose) { @@ -237,7 +237,7 @@ async function closeTicket( } await status.update("Posting close summary..."); - await app.client.api.channels.createMessage(ticket.channelId, await buildCloseChannelMessage(app, closeMessageTokens)); + await app.client.api.channels.createMessage(ticket.channelId, await buildCloseChannelMessage(app, ticketType, closeMessageTokens)); await status.update("Ticket closed."); } @@ -383,6 +383,7 @@ async function moveClosedTicketChannel(app: BotApp, channelId: string) { async function sendCloseDm( app: BotApp, userId: string, + ticketType: ReturnType, tokens: { channelId: string; closerId: string; @@ -400,7 +401,7 @@ async function sendCloseDm( return; } - const messageTemplate = await loadMessageTemplate(app.config.tickets.close.dmMessage ?? DEFAULT_CLOSE_DM_MESSAGE, tokens); + const messageTemplate = await loadMessageTemplate(resolveCloseDmMessageReference(app, ticketType), tokens); await app.client.api.channels .createMessage(dmChannel.id, { @@ -411,6 +412,7 @@ async function sendCloseDm( async function buildCloseChannelMessage( app: BotApp, + ticketType: ReturnType, tokens: { channelId: string; closerId: string; @@ -426,10 +428,13 @@ async function buildCloseChannelMessage( } ) { const deleteButtonCustomId = createCustomId("tickets", "delete-closed"); - const messageTemplate = await loadMessageTemplate(app.config.tickets.close.channelMessage ?? DEFAULT_CLOSE_CHANNEL_MESSAGE, { - ...tokens, - deleteButtonCustomId - }); + const messageTemplate = await loadMessageTemplate( + resolveCloseChannelMessageReference(app, ticketType), + { + ...tokens, + deleteButtonCustomId + } + ); return finalizeMessageTemplate( appendMessageButton( @@ -446,6 +451,14 @@ async function buildCloseChannelMessage( ); } +function resolveCloseDmMessageReference(app: BotApp, ticketType: ReturnType) { + return ticketType.close?.dmMessage ?? app.config.tickets.close.dmMessage ?? DEFAULT_CLOSE_DM_MESSAGE; +} + +function resolveCloseChannelMessageReference(app: BotApp, ticketType: ReturnType) { + return ticketType.close?.channelMessage ?? app.config.tickets.close.channelMessage ?? DEFAULT_CLOSE_CHANNEL_MESSAGE; +} + function createCloseStatusUpdater( app: BotApp, interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction | APIModalSubmitInteraction From d40a3e5431852895d5c15c3e55f9b5ab5aacf824 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:51:12 +0200 Subject: [PATCH 19/67] feat(messages): add default ticket response messages * Add open and close templates for billing tickets * Add open and close templates for general support * Add open and close templates for report tickets --- messages/tickets/ticket-closed-billing.ts | 76 ++++++++++++++++++++ messages/tickets/ticket-closed-dm-billing.ts | 65 +++++++++++++++++ messages/tickets/ticket-closed-dm-general.ts | 61 ++++++++++++++++ messages/tickets/ticket-closed-dm-report.ts | 65 +++++++++++++++++ messages/tickets/ticket-closed-general.ts | 76 ++++++++++++++++++++ messages/tickets/ticket-closed-report.ts | 76 ++++++++++++++++++++ messages/tickets/ticket-opened-billing.ts | 68 ++++++++++++++++++ messages/tickets/ticket-opened-general.ts | 68 ++++++++++++++++++ messages/tickets/ticket-opened-report.ts | 68 ++++++++++++++++++ 9 files changed, 623 insertions(+) create mode 100644 messages/tickets/ticket-closed-billing.ts create mode 100644 messages/tickets/ticket-closed-dm-billing.ts create mode 100644 messages/tickets/ticket-closed-dm-general.ts create mode 100644 messages/tickets/ticket-closed-dm-report.ts create mode 100644 messages/tickets/ticket-closed-general.ts create mode 100644 messages/tickets/ticket-closed-report.ts create mode 100644 messages/tickets/ticket-opened-billing.ts create mode 100644 messages/tickets/ticket-opened-general.ts create mode 100644 messages/tickets/ticket-opened-report.ts diff --git a/messages/tickets/ticket-closed-billing.ts b/messages/tickets/ticket-closed-billing.ts new file mode 100644 index 00000000..104c7e36 --- /dev/null +++ b/messages/tickets/ticket-closed-billing.ts @@ -0,0 +1,76 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const billingTicketClosedMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 16106539, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Billing Ticket Closed" + }, + { + type: ComponentType.TextDisplay, + content: "<@{userId}>'s billing ticket has been closed." + }, + { + type: ComponentType.TextDisplay, + content: "**Close Reason**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Handled By**\n{closerMention}" + }, + { + type: ComponentType.TextDisplay, + content: "**Transcript**\n{transcriptStatus}" + }, + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + custom_id: "{deleteButtonCustomId}", + label: "Delete Ticket", + style: 4 + } + ] + } + ] + } + ] +}; + +export default billingTicketClosedMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-closed-dm-billing.ts b/messages/tickets/ticket-closed-dm-billing.ts new file mode 100644 index 00000000..af59c414 --- /dev/null +++ b/messages/tickets/ticket-closed-dm-billing.ts @@ -0,0 +1,65 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const billingTicketClosedDmMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 16106539, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Your billing ticket has been closed" + }, + { + type: ComponentType.TextDisplay, + content: "If you still need help, open a new billing ticket and include your order details again." + }, + { + type: ComponentType.TextDisplay, + content: "**Reason**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Handled By**\n{closerName}" + }, + { + type: ComponentType.TextDisplay, + content: "**Transcript**\n{transcriptStatus}" + } + ] + } + ] +}; + +export default billingTicketClosedDmMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-closed-dm-general.ts b/messages/tickets/ticket-closed-dm-general.ts new file mode 100644 index 00000000..22e3a9bf --- /dev/null +++ b/messages/tickets/ticket-closed-dm-general.ts @@ -0,0 +1,61 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const generalTicketClosedDmMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 3447003, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Your general support ticket has been closed" + }, + { + type: ComponentType.TextDisplay, + content: "**Reason**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Handled By**\n{closerName}" + }, + { + type: ComponentType.TextDisplay, + content: "**Transcript**\n{transcriptStatus}" + } + ] + } + ] +}; + +export default generalTicketClosedDmMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-closed-dm-report.ts b/messages/tickets/ticket-closed-dm-report.ts new file mode 100644 index 00000000..7a1eb1a7 --- /dev/null +++ b/messages/tickets/ticket-closed-dm-report.ts @@ -0,0 +1,65 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const reportTicketClosedDmMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 15158332, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Your report ticket has been closed" + }, + { + type: ComponentType.TextDisplay, + content: "Staff reviewed the report and any attached evidence." + }, + { + type: ComponentType.TextDisplay, + content: "**Resolution Note**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Handled By**\n{closerName}" + }, + { + type: ComponentType.TextDisplay, + content: "**Transcript**\n{transcriptStatus}" + } + ] + } + ] +}; + +export default reportTicketClosedDmMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-closed-general.ts b/messages/tickets/ticket-closed-general.ts new file mode 100644 index 00000000..a5141f0b --- /dev/null +++ b/messages/tickets/ticket-closed-general.ts @@ -0,0 +1,76 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const generalTicketClosedMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 3447003, + components: [ + { + type: ComponentType.TextDisplay, + content: "## General Support Closed" + }, + { + type: ComponentType.TextDisplay, + content: "<@{userId}>'s general support ticket is now closed." + }, + { + type: ComponentType.TextDisplay, + content: "**Reason**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Claim**\n{claimStatus}" + }, + { + type: ComponentType.TextDisplay, + content: "_Closed by {closerName}_" + }, + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + custom_id: "{deleteButtonCustomId}", + label: "Delete Ticket", + style: 4 + } + ] + } + ] + } + ] +}; + +export default generalTicketClosedMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-closed-report.ts b/messages/tickets/ticket-closed-report.ts new file mode 100644 index 00000000..538a7d2a --- /dev/null +++ b/messages/tickets/ticket-closed-report.ts @@ -0,0 +1,76 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const reportTicketClosedMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 15158332, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Report Case Closed" + }, + { + type: ComponentType.TextDisplay, + content: "The report opened by <@{userId}> has been closed." + }, + { + type: ComponentType.TextDisplay, + content: "**Resolution Note**\n{reason}" + }, + { + type: ComponentType.TextDisplay, + content: "**Claim**\n{claimStatus}" + }, + { + type: ComponentType.TextDisplay, + content: "**Transcript**\n{transcriptStatus}" + }, + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.Button, + custom_id: "{deleteButtonCustomId}", + label: "Delete Ticket", + style: 4 + } + ] + } + ] + } + ] +}; + +export default reportTicketClosedMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-opened-billing.ts b/messages/tickets/ticket-opened-billing.ts new file mode 100644 index 00000000..b7d5766c --- /dev/null +++ b/messages/tickets/ticket-opened-billing.ts @@ -0,0 +1,68 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const billingTicketOpenedMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.TextDisplay, + content: "{createdByMention}{staffMentions}" + }, + { + type: ComponentType.Container, + accent_color: 15844367, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Billing Ticket" + }, + { + type: ComponentType.TextDisplay, + content: "Include invoice numbers, payment method, and any failed transaction details." + }, + { + type: ComponentType.TextDisplay, + content: "**Submitted Details**\n{reason}" + }, + createRuntimeTextSlot(), + { + type: ComponentType.TextDisplay, + content: "**Claim Status**\n{claimStatus}" + }, + createMessageSlot("actions") + ] + } + ] +}; + +export default billingTicketOpenedMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-opened-general.ts b/messages/tickets/ticket-opened-general.ts new file mode 100644 index 00000000..ccd656f8 --- /dev/null +++ b/messages/tickets/ticket-opened-general.ts @@ -0,0 +1,68 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const generalTicketOpenedMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.TextDisplay, + content: "{createdByMention}{staffMentions}" + }, + { + type: ComponentType.Container, + accent_color: 3447003, + components: [ + { + type: ComponentType.TextDisplay, + content: "## General Support Ticket" + }, + { + type: ComponentType.TextDisplay, + content: "A support team member will review this request soon." + }, + { + type: ComponentType.TextDisplay, + content: "**Summary**\n{reason}" + }, + createRuntimeTextSlot(), + { + type: ComponentType.TextDisplay, + content: "**Claim Status**\n{claimStatus}" + }, + createMessageSlot("actions") + ] + } + ] +}; + +export default generalTicketOpenedMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/tickets/ticket-opened-report.ts b/messages/tickets/ticket-opened-report.ts new file mode 100644 index 00000000..e233b7f8 --- /dev/null +++ b/messages/tickets/ticket-opened-report.ts @@ -0,0 +1,68 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const reportTicketOpenedMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.TextDisplay, + content: "{createdByMention}{staffMentions}" + }, + { + type: ComponentType.Container, + accent_color: 15158332, + components: [ + { + type: ComponentType.TextDisplay, + content: "## Report Ticket" + }, + { + type: ComponentType.TextDisplay, + content: "Moderation staff will review the report and any evidence attached." + }, + { + type: ComponentType.TextDisplay, + content: "**Report Details**\n{reason}" + }, + createRuntimeTextSlot(), + { + type: ComponentType.TextDisplay, + content: "**Claim Status**\n{claimStatus}" + }, + createMessageSlot("actions") + ] + } + ] +}; + +export default reportTicketOpenedMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ From fecd4fa757276265dbc18a766c71fcafbe3ef03b Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:06:06 +0200 Subject: [PATCH 20/67] feat(core): add colored logging output * Extract write logic to reduce logger memory overhead. * Use colored ANSI codes for INFO, WARN, ERROR. * Route logs to appropriate console streams. --- src/core/logger.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/logger.ts b/src/core/logger.ts index 840292f4..ac089e75 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -19,27 +19,27 @@ export interface Logger { error(message: string, ...meta: unknown[]): void; } -export function createLogger(scope: string): Logger { - const write = (level: string, message: string, meta: unknown[]) => { - const prefix = `[${scope}] ${level}`; +function write(output: (...data: unknown[]) => void, level: string, message: string, meta: unknown[], scope: string) { + const prefix = `[${scope}] ${level}`; - if (meta.length === 0) { - console.log(prefix, message); - return; - } + if (meta.length === 0) { + output(prefix, message); + return; + } - console.log(prefix, message, ...meta); - }; + output(prefix, message, ...meta); +} +export function createLogger(scope: string): Logger { return { info(message, ...meta) { - write("INFO", message, meta); + write(console.log, "\x1b[39;44mINFO\x1b[0m", message, meta, scope); }, warn(message, ...meta) { - write("WARN", message, meta); + write(console.warn, "\x1b[39;43mWARN\x1b[0m", message, meta, scope); }, error(message, ...meta) { - write("ERROR", message, meta); + write(console.error, "\x1b[39;41mERROR\x1b[0m", message, meta, scope); } }; } From eb2474d416231084e90cfefd1d530f38db881a39 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:06:17 +0200 Subject: [PATCH 21/67] feat(events): add startup environment validation * Validate guildId and bot membership on boot. * Check Administrator permissions and text channel configurations. * Announce bot startup and sponsor list from GitHub. --- src/events/ready.ts | 126 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/src/events/ready.ts b/src/events/ready.ts index 26dd1932..dc66d9db 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -14,13 +14,29 @@ This notice must not be removed, obscured, or replaced. */ import { GatewayDispatchEvents, type GatewayReadyDispatchData, type ToEventProps } from "@discordjs/core"; -import { ActivityType, type GatewayPresenceUpdateData, PresenceUpdateStatus } from "discord-api-types/v10"; +import { + ActivityType, + ChannelType, + type GatewayPresenceUpdateData, + PermissionFlagsBits, + PresenceUpdateStatus +} from "discord-api-types/v10"; import { defineEvent } from "@/core/defineEvent"; import type { BotApp } from "@/core/types"; import { deployApplicationCommands } from "@/deploy-commands"; import { syncTicketPanels } from "@/features/tickets/service"; const PRESENCE_REFRESH_INTERVAL_MS = 900_000; +const SPONSORS_URL = "https://raw.githubusercontent.com/Sayrix/sponsors/main/sponsors.json"; +const PANEL_CHANNEL_TYPES = new Set([ + ChannelType.GuildAnnouncement, + ChannelType.GuildStageVoice, + ChannelType.GuildText, + ChannelType.GuildVoice, + ChannelType.AnnouncementThread, + ChannelType.PrivateThread, + ChannelType.PublicThread +]); const ACTIVITY_TYPES = { COMPETING: ActivityType.Competing, CUSTOM: ActivityType.Custom, @@ -40,6 +56,7 @@ const readyEvent = defineEvent<[ToEventProps]>({ name: GatewayDispatchEvents.Ready, once: true, async execute(app, event) { + await validateStartupEnvironment(app, event.data.user.id); app.logger.info(`Connected as ${event.data.user.username}.`); await deployApplicationCommands({ @@ -54,11 +71,118 @@ const readyEvent = defineEvent<[ToEventProps]>({ setInterval(() => { void applyConfiguredPresence(app); }, PRESENCE_REFRESH_INTERVAL_MS); + await announceStartup(app, `${event.data.user.username}#${event.data.user.discriminator}`, event.data.user.id); } }); export default readyEvent; +async function validateStartupEnvironment(app: BotApp, currentUserId: string) { + const guildId = app.config.guildId.trim(); + + if (!guildId) { + await failStartup(app, 'Please set "guildId" in config/config.ts before starting the bot.'); + } + + await app.client.api.guilds + .get(guildId) + .catch((error) => failStartup(app, `Configured guild "${guildId}" was not found or is not accessible.`, error)); + + const member = await app.client.api.guilds + .getMember(guildId, currentUserId) + .catch((error) => failStartup(app, `Bot user "${currentUserId}" is not a member of guild "${guildId}".`, error)); + + const roles = await app.client.api.guilds + .getRoles(guildId) + .catch((error) => failStartup(app, `Failed to fetch roles for guild "${guildId}".`, error)); + const memberRoleIds = new Set([guildId, ...member.roles]); + const permissions = roles.reduce((bits, role) => { + if (!memberRoleIds.has(role.id)) { + return bits; + } + + return bits | BigInt(role.permissions); + }, 0n); + + if ((permissions & PermissionFlagsBits.Administrator) !== PermissionFlagsBits.Administrator) { + warnStartup(app, "The bot does not have the Administrator permission. Some actions may fail because of missing permissions."); + } + + for (const [panelKey, panel] of Object.entries(app.config.panels)) { + const channelId = panel.channelId.trim(); + + if (!channelId) { + await failStartup(app, `Panel "${panelKey}" is missing its channelId.`); + } + + const channel = await app.client.api.channels + .get(channelId) + .catch((error) => failStartup(app, `Panel "${panelKey}" channel "${channelId}" was not found.`, error)); + + if (!PANEL_CHANNEL_TYPES.has(channel.type)) { + await failStartup(app, `Panel "${panelKey}" channel "${channelId}" is not a text-based channel.`); + } + } +} + +async function failStartup(app: BotApp, message: string, error?: unknown): Promise { + if (error) { + app.logger.error(message, error); + } else { + app.logger.error(message); + } + + process.exit(1); +} + +function warnStartup(app: BotApp, message: string, error?: unknown) { + if (error) { + app.logger.warn(message, error); + return; + } + + app.logger.warn(message); +} + +async function announceStartup(app: BotApp, tag: string, userId: string) { + app.logger.info(`🚀 The bot is ready! Logged in as ${tag} (${userId}).`); + app.logger.info("⭐ Help the project by leaving a star on GitHub: \x1b[36;1mhttps://github.com/Sayrix/Ticket-Bot\x1b[0m"); + app.logger.info( + "⛅ Need to host your Ticket-Bot? Support the project and get access to hosting for $1/month: \x1b[36;1mhttps://github.com/sponsors/Sayrix\x1b[0m" + ); + + const sponsorLogins = await fetchSponsors(); + + if (sponsorLogins.length === 0) { + return; + } + + const sponsorNames = sponsorLogins.map( + (login) => `\x1b]8;;https://github.com/${login}\x1b\\\x1b[1m${login}\x1b]8;;\x1b\\\x1b[0m` + ); + app.logger.info(`💖 Thanks to our sponsors: ${sponsorNames.join(", ")} who make this project possible!`); +} + +async function fetchSponsors() { + try { + const response = (await fetch(SPONSORS_URL)) as any; + + if (!response.ok) { + return []; + } + + const payload = (await response.json()) as Array<{ + sponsor?: { + login?: string; + }; + }>; + + return payload.flatMap((entry) => (typeof entry.sponsor?.login === "string" ? [entry.sponsor.login] : [])); + } catch { + return []; + } +} + async function applyConfiguredPresence(app: BotApp) { const configuredStatus = app.config.status; From 6ae1e401476e3e32c29be8bfb782bdc6b6750d67 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:06:18 +0200 Subject: [PATCH 22/67] feat(core): track version and check for updates * Introduce version.ts for runtime tracking. * Add ASCII art banner on startup. * Implement update polling from GitHub API tags. * Replace generic console.error with logger. --- src/index.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++++++- src/version.ts | 33 +++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/version.ts diff --git a/src/index.ts b/src/index.ts index 6d96928b..efdd4718 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,8 +15,24 @@ This notice must not be removed, obscured, or replaced. import { config } from "dotenv"; import { createBotApp } from "@/app"; +import { createLogger } from "@/core/logger"; +import { BOT_VERSION } from "@/version"; + +const REPOSITORY_TAGS_URL = "https://api.github.com/repos/Sayrix/Ticket-Bot/tags"; +const VERSION_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)$/; +const logger = createLogger("boot"); + +console.log(` +\x1b[38;2;143;110;250m████████╗██╗ ██████╗██╗ ██╗███████╗████████╗ ██████╗ ██████╗ ████████╗ +\x1b[38;2;157;101;254m╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝ ██╔══██╗██╔═══██╗╚══██╔══╝ +\x1b[38;2;172;90;255m ██║ ██║██║ █████╔╝ █████╗ ██║ ██████╔╝██║ ██║ ██║ +\x1b[38;2;188;76;255m ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ██╔══██╗██║ ██║ ██║ +\x1b[38;2;205;54;255m ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ██████╔╝╚██████╔╝ ██║ +\x1b[38;2;222;0;255m ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝\x1b[0m +`); config({ path: "./config/.env", quiet: true }); +void checkForUpdates(); async function main() { const { start, stop } = await createBotApp(); @@ -33,10 +49,70 @@ async function main() { } main().catch(async (error) => { - console.error("[boot] Failed to start bot", error); + logger.error("Failed to start bot", error); process.exit(1); }); +async function checkForUpdates() { + try { + const response = (await fetch(REPOSITORY_TAGS_URL, { + headers: { + accept: "application/vnd.github+json" + } + })) as any; + + if (!response.ok) { + logger.warn(`Failed to pull latest version from server (${response.status}).`); + return; + } + + const tags = (await response.json()) as Array<{ name?: string }>; + const latestTag = tags.find((tag) => parseVersion(tag.name)); + const latestVersion = latestTag ? parseVersion(latestTag.name) : null; + const currentVersion = parseVersion(BOT_VERSION); + + if (!latestTag?.name || !latestVersion || !currentVersion) { + logger.warn("Failed to parse repository tags for update checking."); + return; + } + + if (compareVersions(latestVersion, currentVersion) > 0) { + logger.warn(`🔄️ New version available: ${latestTag.name}; current version: ${BOT_VERSION}.`); + return; + } + + logger.info(`Ticket-Bot is up to date (${BOT_VERSION}). Latest tag: ${latestTag.name}.`); + } catch (error) { + logger.warn("Failed to check for updates.", error); + } +} + +function parseVersion(value: string | undefined) { + if (!value) { + return null; + } + + const match = VERSION_PATTERN.exec(value.trim()); + + if (!match) { + return null; + } + + return match.slice(1).map((segment) => Number.parseInt(segment, 10)); +} + +function compareVersions(left: number[], right: number[]) { + for (let index = 0; index < Math.max(left.length, right.length); index += 1) { + const delta = (left[index] ?? 0) - (right[index] ?? 0); + + if (delta !== 0) { + return delta; + } + } + + return 0; +} + /* Ticket-Bot is licensed under the GNU Affero General Public License, version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 00000000..13eb54d2 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,33 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +// Keep the runtime version in source so source-only updates still report correctly. (for example, +// if someone only updates the src folder and not the package.json, or if the package.json version is not updated for some reason) +export const BOT_VERSION = "4.0.0"; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ From 631935c5416cc7d1478a56411b365b3a45d5bf96 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:54:29 +0200 Subject: [PATCH 23/67] feat(logs): add ticket audit logging service and templates * Add logs configuration to config.example.ts and internal typings * Create enqueueTicketLog service and logging utility functions * Add Discord message templates for audited ticket events * Fix Component V2 text display handling in messages util --- config/config.example.ts | 17 +++ messages/logs/ticket-claimed.ts | 50 ++++++++ messages/logs/ticket-closed.ts | 53 +++++++++ messages/logs/ticket-created.ts | 51 ++++++++ messages/logs/ticket-deleted.ts | 53 +++++++++ messages/logs/ticket-renamed.ts | 51 ++++++++ messages/logs/ticket-unclaimed.ts | 50 ++++++++ messages/logs/user-added.ts | 49 ++++++++ messages/logs/user-removed.ts | 49 ++++++++ src/config/index.ts | 14 +++ src/features/logs/service.ts | 191 ++++++++++++++++++++++++++++++ src/features/logs/types.ts | 97 +++++++++++++++ src/features/logs/utils.ts | 47 ++++++++ src/features/tickets/messages.ts | 16 ++- src/features/tickets/types.ts | 2 + src/types/config.d.ts | 2 - 16 files changed, 784 insertions(+), 8 deletions(-) create mode 100644 messages/logs/ticket-claimed.ts create mode 100644 messages/logs/ticket-closed.ts create mode 100644 messages/logs/ticket-created.ts create mode 100644 messages/logs/ticket-deleted.ts create mode 100644 messages/logs/ticket-renamed.ts create mode 100644 messages/logs/ticket-unclaimed.ts create mode 100644 messages/logs/user-added.ts create mode 100644 messages/logs/user-removed.ts create mode 100644 src/features/logs/service.ts create mode 100644 src/features/logs/types.ts create mode 100644 src/features/logs/utils.ts diff --git a/config/config.example.ts b/config/config.example.ts index 0901b9a4..660a4e30 100644 --- a/config/config.example.ts +++ b/config/config.example.ts @@ -24,6 +24,23 @@ export default defineConfig("0.0.1", { // Transcript ID style used by ticket.pm uploads. // "uuid" matches the current default. "emoji" keeps the older style. uuidType: "uuid", + logs: { + // Set to true to post audit logs for ticket actions. + enabled: true, + // Channel where ticket audit logs will be sent. + channelId: "171717171717171717", + // Omit this object to enable every supported log type. + events: { + ticketCreate: true, + ticketClaim: true, + ticketUnclaim: true, + ticketClose: true, + ticketDelete: true, + userAdded: true, + userRemoved: true, + ticketRename: true + } + }, status: { // Set to false to leave the bot presence untouched. enabled: true, diff --git a/messages/logs/ticket-claimed.ts b/messages/logs/ticket-claimed.ts new file mode 100644 index 00000000..bb94ae64 --- /dev/null +++ b/messages/logs/ticket-claimed.ts @@ -0,0 +1,50 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketClaimedLogMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 16426522, + components: [ + { type: ComponentType.TextDisplay, content: "## Ticket Claimed" }, + { type: ComponentType.TextDisplay, content: "{actorMention} claimed {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, + { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, + { type: ComponentType.TextDisplay, content: "**Open Age**\n{ticketAge}" } + ] + } + ] +}; + +export default ticketClaimedLogMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/logs/ticket-closed.ts b/messages/logs/ticket-closed.ts new file mode 100644 index 00000000..28ed4a0d --- /dev/null +++ b/messages/logs/ticket-closed.ts @@ -0,0 +1,53 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketClosedLogMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 16007990, + components: [ + { type: ComponentType.TextDisplay, content: "## Ticket Closed" }, + { type: ComponentType.TextDisplay, content: "{actorMention} closed {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, + { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, + { type: ComponentType.TextDisplay, content: "**Claim Status**\n{claimStatus}" }, + { type: ComponentType.TextDisplay, content: "**Open Age**\n{ticketAge}" }, + { type: ComponentType.TextDisplay, content: "**Reason**\n{reason}" }, + { type: ComponentType.TextDisplay, content: "**Transcript**\n{transcriptStatus}" } + ] + } + ] +}; + +export default ticketClosedLogMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/logs/ticket-created.ts b/messages/logs/ticket-created.ts new file mode 100644 index 00000000..3aec96a1 --- /dev/null +++ b/messages/logs/ticket-created.ts @@ -0,0 +1,51 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketCreatedLogMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 3901635, + components: [ + { type: ComponentType.TextDisplay, content: "## Ticket Created" }, + { type: ComponentType.TextDisplay, content: "{actorMention} opened {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, + { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, + { type: ComponentType.TextDisplay, content: "**Created**\n{createdAt}" }, + { type: ComponentType.TextDisplay, content: "**Reason**\n{reason}" } + ] + } + ] +}; + +export default ticketCreatedLogMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/logs/ticket-deleted.ts b/messages/logs/ticket-deleted.ts new file mode 100644 index 00000000..c6db0b94 --- /dev/null +++ b/messages/logs/ticket-deleted.ts @@ -0,0 +1,53 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketDeletedLogMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 13632027, + components: [ + { type: ComponentType.TextDisplay, content: "## Ticket Deleted" }, + { type: ComponentType.TextDisplay, content: "{actorMention} deleted {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, + { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, + { type: ComponentType.TextDisplay, content: "**Claim Status**\n{claimStatus}" }, + { type: ComponentType.TextDisplay, content: "**Open Age**\n{ticketAge}" }, + { type: ComponentType.TextDisplay, content: "**Close Reason**\n{reason}" }, + { type: ComponentType.TextDisplay, content: "**Transcript**\n{transcriptStatus}" } + ] + } + ] +}; + +export default ticketDeletedLogMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/logs/ticket-renamed.ts b/messages/logs/ticket-renamed.ts new file mode 100644 index 00000000..4e324091 --- /dev/null +++ b/messages/logs/ticket-renamed.ts @@ -0,0 +1,51 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketRenamedLogMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 3447003, + components: [ + { type: ComponentType.TextDisplay, content: "## Ticket Renamed" }, + { type: ComponentType.TextDisplay, content: "{actorMention} renamed {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, + { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, + { type: ComponentType.TextDisplay, content: "**From**\n`{oldChannelName}`" }, + { type: ComponentType.TextDisplay, content: "**To**\n`{newChannelName}`" } + ] + } + ] +}; + +export default ticketRenamedLogMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/logs/ticket-unclaimed.ts b/messages/logs/ticket-unclaimed.ts new file mode 100644 index 00000000..88959116 --- /dev/null +++ b/messages/logs/ticket-unclaimed.ts @@ -0,0 +1,50 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const ticketUnclaimedLogMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 9807270, + components: [ + { type: ComponentType.TextDisplay, content: "## Ticket Unclaimed" }, + { type: ComponentType.TextDisplay, content: "{actorMention} unclaimed {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, + { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, + { type: ComponentType.TextDisplay, content: "**Open Age**\n{ticketAge}" } + ] + } + ] +}; + +export default ticketUnclaimedLogMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/logs/user-added.ts b/messages/logs/user-added.ts new file mode 100644 index 00000000..b05d5865 --- /dev/null +++ b/messages/logs/user-added.ts @@ -0,0 +1,49 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const userAddedLogMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 3901635, + components: [ + { type: ComponentType.TextDisplay, content: "## User Added" }, + { type: ComponentType.TextDisplay, content: "{actorMention} added {targetMention} to {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, + { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" } + ] + } + ] +}; + +export default userAddedLogMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/messages/logs/user-removed.ts b/messages/logs/user-removed.ts new file mode 100644 index 00000000..861b8298 --- /dev/null +++ b/messages/logs/user-removed.ts @@ -0,0 +1,49 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { ComponentType } from "@discordjs/core"; +import type { LoadedMessageTemplate } from "@/features/tickets/types"; + +const userRemovedLogMessage: LoadedMessageTemplate = { + components: [ + { + type: ComponentType.Container, + accent_color: 16007990, + components: [ + { type: ComponentType.TextDisplay, content: "## User Removed" }, + { type: ComponentType.TextDisplay, content: "{actorMention} removed {targetMention} from {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, + { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" } + ] + } + ] +}; + +export default userRemovedLogMessage; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/config/index.ts b/src/config/index.ts index e6def4d6..50ba07d6 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -22,6 +22,20 @@ interface ConfigV0_0_1 { lang: "en"; /** Controls the transcript ID style when uploading to ticket.pm */ uuidType?: "uuid" | "emoji"; + logs: { + enabled: boolean; + channelId: string; + events?: { + ticketCreate?: boolean; + ticketClaim?: boolean; + ticketUnclaim?: boolean; + ticketClose?: boolean; + ticketDelete?: boolean; + userAdded?: boolean; + userRemoved?: boolean; + ticketRename?: boolean; + }; + }; status?: { enabled: boolean; text?: string; diff --git a/src/features/logs/service.ts b/src/features/logs/service.ts new file mode 100644 index 00000000..d499d0f3 --- /dev/null +++ b/src/features/logs/service.ts @@ -0,0 +1,191 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import type { BotApp } from "@/core/types"; +import type { TicketLogEvent } from "@/features/logs/types"; +import { DEFAULT_NO_REASON } from "@/features/tickets/constants"; +import { finalizeMessageTemplate, loadMessageTemplate } from "@/features/tickets/messages"; +import type { LogEventToggleKey } from "@/features/tickets/types"; + +const LOG_EVENT_TOGGLE_KEYS: Record = { + ticketCreate: "ticketCreate", + ticketClaim: "ticketClaim", + ticketUnclaim: "ticketUnclaim", + ticketClose: "ticketClose", + ticketDelete: "ticketDelete", + userAdded: "userAdded", + userRemoved: "userRemoved", + ticketRename: "ticketRename" +}; + +const LOG_TEMPLATE_REFERENCES: Record = { + ticketCreate: "logs/ticket-created", + ticketClaim: "logs/ticket-claimed", + ticketUnclaim: "logs/ticket-unclaimed", + ticketClose: "logs/ticket-closed", + ticketDelete: "logs/ticket-deleted", + userAdded: "logs/user-added", + userRemoved: "logs/user-removed", + ticketRename: "logs/ticket-renamed" +}; + +export async function sendTicketLog(app: BotApp, event: TicketLogEvent) { + if (!shouldSendTicketLog(app, event.kind)) { + if (!app.config.logs.channelId.trim()) { + app.logger.warn(`Skipping ${event.kind} audit log because logs.channelId is empty.`); + } + return; + } + + const channelId = app.config.logs.channelId.trim(); + + try { + const messageTemplate = await loadMessageTemplate(LOG_TEMPLATE_REFERENCES[event.kind], createLogTokens(event)); + const payload = finalizeMessageTemplate({ + ...messageTemplate, + allowed_mentions: messageTemplate.allowed_mentions ?? { + parse: [] + } + }); + + await app.client.api.channels.createMessage(channelId, payload); + } catch (error) { + app.logger.warn(`Failed to send ${event.kind} audit log.`, error); + } +} + +export function shouldSendTicketLog(app: BotApp, kind: TicketLogEvent["kind"]) { + if (!app.config.logs.enabled) { + return false; + } + + if (!app.config.logs.channelId.trim()) { + return false; + } + + const toggleKey = LOG_EVENT_TOGGLE_KEYS[kind]; + return app.config.logs.events?.[toggleKey] ?? true; +} + +function createLogTokens(event: TicketLogEvent) { + const openedAtSeconds = Math.floor(event.ticket.createdAt / 1000); + const claimedById = resolveClaimedById(event); + const tokens: Record = { + actorId: event.actor.id, + actorMention: `<@${event.actor.id}>`, + actorName: event.actor.username, + claimStatus: claimedById ? `Claimed by <@${claimedById}>` : "Unclaimed", + claimerId: claimedById ?? undefined, + claimerMention: claimedById ? `<@${claimedById}>` : undefined, + createdAt: ``, + createdById: event.ticket.createdById, + createdByMention: `<@${event.ticket.createdById}>`, + reason: DEFAULT_NO_REASON, + targetId: undefined, + targetMention: undefined, + ticketAge: formatDuration(Date.now() - event.ticket.createdAt), + ticketChannelId: event.ticket.ticketChannelId, + ticketChannelMention: `<#${event.ticket.ticketChannelId}>`, + ticketId: event.ticket.ticketId, + ticketTypeKey: event.ticket.ticketTypeKey, + ticketTypeName: event.ticket.ticketTypeName, + transcriptStatus: "Unavailable or still processing.", + transcriptUrl: undefined + }; + + switch (event.kind) { + case "ticketCreate": + tokens.reason = event.reason; + break; + case "ticketClose": + case "ticketDelete": + tokens.reason = event.reason; + tokens.transcriptUrl = event.transcriptUrl ?? undefined; + tokens.transcriptStatus = event.transcriptUrl + ? `[Open Transcript](${event.transcriptUrl})` + : "Unavailable or still processing."; + break; + case "userAdded": + case "userRemoved": + tokens.targetId = event.targetId; + tokens.targetMention = `<@${event.targetId}>`; + break; + case "ticketRename": + tokens.oldChannelName = event.oldChannelName; + tokens.newChannelName = event.newChannelName; + break; + } + + return tokens; +} + +function resolveClaimedById(event: TicketLogEvent) { + switch (event.kind) { + case "ticketClaim": + case "ticketUnclaim": + return event.actor.id; + default: + return event.ticket.claimedById ?? undefined; + } +} + +function formatDuration(durationMs: number) { + const totalSeconds = Math.max(0, Math.floor(durationMs / 1000)); + + if (totalSeconds < 60) { + return `${totalSeconds}s`; + } + + const units: Array<[label: string, seconds: number]> = [ + ["d", 86_400], + ["h", 3_600], + ["m", 60], + ["s", 1] + ]; + const parts: string[] = []; + let remainingSeconds = totalSeconds; + + for (const [label, unitSeconds] of units) { + if (parts.length === 2) { + break; + } + + const unitValue = Math.floor(remainingSeconds / unitSeconds); + + if (unitValue <= 0) { + continue; + } + + parts.push(`${unitValue}${label}`); + remainingSeconds -= unitValue * unitSeconds; + } + + return parts.join(" "); +} + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/logs/types.ts b/src/features/logs/types.ts new file mode 100644 index 00000000..ca4c539f --- /dev/null +++ b/src/features/logs/types.ts @@ -0,0 +1,97 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import type { APIUser } from "@discordjs/core"; + +export interface TicketLogTicketContext { + ticketId: string; + ticketChannelId: string; + ticketTypeKey: string; + ticketTypeName: string; + createdAt: number; + createdById: string; + claimedById?: string | null; +} + +interface BaseTicketLogEvent { + actor: APIUser; + ticket: TicketLogTicketContext; +} + +export interface TicketCreateLogEvent extends BaseTicketLogEvent { + kind: "ticketCreate"; + reason: string; +} + +export interface TicketClaimLogEvent extends BaseTicketLogEvent { + kind: "ticketClaim"; +} + +export interface TicketUnclaimLogEvent extends BaseTicketLogEvent { + kind: "ticketUnclaim"; +} + +export interface TicketCloseLogEvent extends BaseTicketLogEvent { + kind: "ticketClose"; + reason: string; + transcriptUrl?: string | null; +} + +export interface TicketDeleteLogEvent extends BaseTicketLogEvent { + kind: "ticketDelete"; + reason: string; + transcriptUrl?: string | null; +} + +export interface UserAddedLogEvent extends BaseTicketLogEvent { + kind: "userAdded"; + targetId: string; +} + +export interface UserRemovedLogEvent extends BaseTicketLogEvent { + kind: "userRemoved"; + targetId: string; +} + +export interface TicketRenameLogEvent extends BaseTicketLogEvent { + kind: "ticketRename"; + newChannelName: string; + oldChannelName: string; +} + +export type TicketLogEvent = + | TicketCreateLogEvent + | TicketClaimLogEvent + | TicketUnclaimLogEvent + | TicketCloseLogEvent + | TicketDeleteLogEvent + | UserAddedLogEvent + | UserRemovedLogEvent + | TicketRenameLogEvent; + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/logs/utils.ts b/src/features/logs/utils.ts new file mode 100644 index 00000000..7a5cb382 --- /dev/null +++ b/src/features/logs/utils.ts @@ -0,0 +1,47 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import type { TicketRecord } from "@/db/schema"; +import type { TicketLogTicketContext } from "@/features/logs/types"; + +export function createTicketLogContext( + ticket: Pick, + ticketTypeName: string +): TicketLogTicketContext { + return { + ticketId: ticket.id.toString(), + ticketChannelId: ticket.channelId, + ticketTypeKey: ticket.type, + ticketTypeName, + createdAt: ticket.createdAt, + createdById: ticket.createdBy, + claimedById: ticket.claimedBy + }; +} + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/features/tickets/messages.ts b/src/features/tickets/messages.ts index 9ccd315e..398a4898 100644 --- a/src/features/tickets/messages.ts +++ b/src/features/tickets/messages.ts @@ -84,12 +84,16 @@ export function appendMessageText( if (usesComponentsV2(payload)) { // Components V2 messages cannot use the legacy `content` field, so extra // runtime text is injected as a text display block instead. - return appendMessageComponents(payload, [ - { - type: ComponentType.TextDisplay, - content: normalizedText - } - ], options?.slot); + return appendMessageComponents( + payload, + [ + { + type: ComponentType.TextDisplay, + content: normalizedText + } + ], + options?.slot + ); } return { diff --git a/src/features/tickets/types.ts b/src/features/tickets/types.ts index e4963de6..695b41b6 100644 --- a/src/features/tickets/types.ts +++ b/src/features/tickets/types.ts @@ -25,6 +25,8 @@ export type ButtonPanelEntryConfig = Extract["style"]>; export type TicketClaimsConfig = CurrentConfig["tickets"]["claims"]; export type TicketClaimMode = TicketClaimsConfig["mode"]; +export type LogsConfig = CurrentConfig["logs"]; +export type LogEventToggleKey = keyof NonNullable; export interface LoadedMessageTemplate { allowed_mentions?: APIAllowedMentions; diff --git a/src/types/config.d.ts b/src/types/config.d.ts index cb81edab..fe974ef5 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -13,8 +13,6 @@ project repository or to its website. This notice must not be removed, obscured, or replaced. */ - - /* Ticket-Bot is licensed under the GNU Affero General Public License, version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. From 4acb48440618a022472a0dff1ceaad92147f8daf Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:54:45 +0200 Subject: [PATCH 24/67] feat(tickets): integrate audit logging into actions * Send audit logs on ticket creation, claim, and unclaim * Send audit logs when a ticket is closed or deleted * Send audit logs on user additions or removals * Send audit logs on ticket channel rename --- src/features/commands/add/command.ts | 9 +++++ src/features/commands/mass_add/command.ts | 14 ++++++++ src/features/commands/remove/command.ts | 40 +++++++++++++++++++---- src/features/commands/rename/command.ts | 26 ++++++++++++++- src/features/tickets/claim-workflow.ts | 36 +++++++++++--------- src/features/tickets/close-workflow.ts | 40 ++++++++++++++++++----- src/features/tickets/ticket-workflow.ts | 21 +++++++++--- 7 files changed, 151 insertions(+), 35 deletions(-) diff --git a/src/features/commands/add/command.ts b/src/features/commands/add/command.ts index 91cfecb5..b67b48d2 100644 --- a/src/features/commands/add/command.ts +++ b/src/features/commands/add/command.ts @@ -18,6 +18,8 @@ import { ApplicationCommandOptionType } from "discord-api-types/v10"; import { defineCommand } from "@/core/defineCommand"; import { reply } from "@/core/respond"; import { getUserOption } from "@/features/commands/shared/options"; +import { sendTicketLog } from "@/features/logs/service"; +import { createTicketLogContext } from "@/features/logs/utils"; import { getInvitedUserIds, grantTicketParticipantAccess, @@ -25,6 +27,7 @@ import { updateInvitedUserIds } from "@/features/tickets/participants"; import { getOpenTicketByChannel } from "@/features/tickets/records"; +import { getInteractionUser } from "@/features/tickets/utils"; export default defineCommand({ data: { @@ -88,6 +91,12 @@ export default defineCommand({ await grantTicketParticipantAccess(app, openTicket.ticket.channelId, selectedUser.userId); await updateInvitedUserIds(app, openTicket.ticket.channelId, [...invitedUserIds, selectedUser.userId]); + void sendTicketLog(app, { + kind: "userAdded", + actor: getInteractionUser(interaction), + targetId: selectedUser.userId, + ticket: createTicketLogContext(openTicket.ticket, openTicket.ticketType.name) + }); await reply(app, interaction, { content: `Added <@${selectedUser.userId}> to this ticket.`, diff --git a/src/features/commands/mass_add/command.ts b/src/features/commands/mass_add/command.ts index b5d11587..a3369de4 100644 --- a/src/features/commands/mass_add/command.ts +++ b/src/features/commands/mass_add/command.ts @@ -18,6 +18,8 @@ import { ApplicationCommandOptionType } from "discord-api-types/v10"; import { defineCommand } from "@/core/defineCommand"; import { reply } from "@/core/respond"; import { getStringOption } from "@/features/commands/shared/options"; +import { sendTicketLog } from "@/features/logs/service"; +import { createTicketLogContext } from "@/features/logs/utils"; import { getInvitedUserIds, grantTicketParticipantAccess, @@ -25,6 +27,7 @@ import { updateInvitedUserIds } from "@/features/tickets/participants"; import { getOpenTicketByChannel } from "@/features/tickets/records"; +import { getInteractionUser } from "@/features/tickets/utils"; export default defineCommand({ data: { @@ -93,6 +96,17 @@ export default defineCommand({ if (addedUserIds.length > 0) { await updateInvitedUserIds(app, openTicket.ticket.channelId, nextInvitedUserIds); + const actor = getInteractionUser(interaction); + const ticketLogContext = createTicketLogContext(openTicket.ticket, openTicket.ticketType.name); + + for (const userId of addedUserIds) { + void sendTicketLog(app, { + kind: "userAdded", + actor, + targetId: userId, + ticket: ticketLogContext + }); + } } await reply(app, interaction, { diff --git a/src/features/commands/remove/command.ts b/src/features/commands/remove/command.ts index 2a773e8a..781a6f58 100644 --- a/src/features/commands/remove/command.ts +++ b/src/features/commands/remove/command.ts @@ -20,9 +20,13 @@ import { createCustomId } from "@/core/custom-id"; import { defineCommand } from "@/core/defineCommand"; import { reply, updateMessage } from "@/core/respond"; import type { ComponentExecutionContext } from "@/core/types"; +import type { TicketRecord } from "@/db/schema"; import { getUserOption } from "@/features/commands/shared/options"; +import { sendTicketLog } from "@/features/logs/service"; +import { createTicketLogContext } from "@/features/logs/utils"; import { getInvitedUserIds, revokeTicketParticipantAccess, updateInvitedUserIds } from "@/features/tickets/participants"; import { getOpenTicketByChannel } from "@/features/tickets/records"; +import { getInteractionUser } from "@/features/tickets/utils"; const REMOVE_USERS_CUSTOM_ID = createCustomId("tickets", "remove-users"); @@ -63,7 +67,9 @@ export default defineCommand({ const selectedUser = getUserOption(interaction, "user"); if (selectedUser) { - await removeUsersFromTicket(app, interaction, invitedUserIds, openTicket.ticket.channelId, [selectedUser.userId]); + await removeUsersFromTicket(app, interaction, openTicket.ticket, openTicket.ticketType.name, invitedUserIds, [ + selectedUser.userId + ]); return; } @@ -117,16 +123,25 @@ export async function handleRemoveUsersSelect(context: ComponentExecutionContext const invitedUserIds = getInvitedUserIds(openTicket.ticket); const selectedUserIds = interaction.data.values.filter((userId: string) => invitedUserIds.includes(userId)); - await removeUsersFromTicket(context.app, interaction, invitedUserIds, openTicket.ticket.channelId, selectedUserIds, { - responseMode: "update-message" - }); + await removeUsersFromTicket( + context.app, + interaction, + openTicket.ticket, + openTicket.ticketType.name, + invitedUserIds, + selectedUserIds, + { + responseMode: "update-message" + } + ); } async function removeUsersFromTicket( app: ComponentExecutionContext["app"], interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction, + ticket: TicketRecord, + ticketTypeName: string, invitedUserIds: string[], - channelId: string, selectedUserIds: string[], options?: { responseMode?: "reply" | "update-message"; @@ -140,14 +155,25 @@ async function removeUsersFromTicket( } for (const userId of removableUserIds) { - await revokeTicketParticipantAccess(app, channelId, userId); + await revokeTicketParticipantAccess(app, ticket.channelId, userId); } await updateInvitedUserIds( app, - channelId, + ticket.channelId, invitedUserIds.filter((userId) => !removableUserIds.includes(userId)) ); + const actor = getInteractionUser(interaction); + const ticketLogContext = createTicketLogContext(ticket, ticketTypeName); + + for (const userId of removableUserIds) { + void sendTicketLog(app, { + kind: "userRemoved", + actor, + targetId: userId, + ticket: ticketLogContext + }); + } await respond( app, diff --git a/src/features/commands/rename/command.ts b/src/features/commands/rename/command.ts index 41555631..1ae3f353 100644 --- a/src/features/commands/rename/command.ts +++ b/src/features/commands/rename/command.ts @@ -18,9 +18,11 @@ import { ApplicationCommandOptionType } from "discord-api-types/v10"; import { defineCommand } from "@/core/defineCommand"; import { reply } from "@/core/respond"; import { getStringOption } from "@/features/commands/shared/options"; +import { sendTicketLog, shouldSendTicketLog } from "@/features/logs/service"; +import { createTicketLogContext } from "@/features/logs/utils"; import { hasTicketStaffAccess } from "@/features/tickets/config-access"; import { getOpenTicketByChannel } from "@/features/tickets/records"; -import { getMemberRoleIds, sanitizeChannelName } from "@/features/tickets/utils"; +import { getInteractionUser, getMemberRoleIds, sanitizeChannelName } from "@/features/tickets/utils"; export default defineCommand({ data: { @@ -65,9 +67,31 @@ export default defineCommand({ } const nextName = sanitizeChannelName(requestedName); + const ticketLogContext = createTicketLogContext(openTicket.ticket, openTicket.ticketType.name); + let previousName = openTicket.ticket.channelId; + + if (shouldSendTicketLog(app, "ticketRename")) { + const currentChannel = await app.client.api.channels.get(openTicket.ticket.channelId).catch(() => null); + previousName = + currentChannel && "name" in currentChannel && typeof currentChannel.name === "string" + ? currentChannel.name + : previousName; + } + await app.client.api.channels.edit(openTicket.ticket.channelId, { name: nextName }); + const actor = getInteractionUser(interaction); + + if (shouldSendTicketLog(app, "ticketRename")) { + void sendTicketLog(app, { + kind: "ticketRename", + actor, + oldChannelName: previousName, + newChannelName: nextName, + ticket: ticketLogContext + }); + } await reply(app, interaction, { content: `Ticket renamed to <#${openTicket.ticket.channelId}>.`, diff --git a/src/features/tickets/claim-workflow.ts b/src/features/tickets/claim-workflow.ts index 7f543168..a0fe50d9 100644 --- a/src/features/tickets/claim-workflow.ts +++ b/src/features/tickets/claim-workflow.ts @@ -18,7 +18,10 @@ import { MessageFlags } from "@discordjs/core"; import { eq } from "drizzle-orm"; import { reply } from "@/core/respond"; import type { BotApp, CommandExecutionContext, ComponentExecutionContext } from "@/core/types"; +import type { TicketRecord } from "@/db/schema"; import { ticketsTable } from "@/db/schema"; +import { sendTicketLog } from "@/features/logs/service"; +import { createTicketLogContext } from "@/features/logs/utils"; import { hasTicketStaffAccess } from "@/features/tickets/config-access"; import { getOpenTicketByChannel } from "@/features/tickets/records"; import { syncTicketWelcomeMessage } from "@/features/tickets/ticket-workflow"; @@ -92,6 +95,11 @@ async function claimTicket(app: BotApp, interaction: ClaimInteraction) { await updateClaimedTicketPresentation(app, nextTicketState, ticketType.name, actor.username); await syncTicketWelcomeMessage(app, nextTicketState, ticketType); + void sendTicketLog(app, { + kind: "ticketClaim", + actor, + ticket: createTicketLogContext(nextTicketState, ticketType.name) + }); await replyWithContent( app, @@ -148,6 +156,17 @@ async function unclaimTicket(app: BotApp, interaction: ClaimInteraction) { }, ticketType ); + void sendTicketLog(app, { + kind: "ticketUnclaim", + actor, + ticket: createTicketLogContext( + { + ...ticket, + claimedBy: null + }, + ticketType.name + ) + }); await replyWithContent(app, interaction, "You unclaimed this ticket."); } @@ -190,14 +209,7 @@ function canTakeOverClaim(app: BotApp, roleIds: string[]) { async function updateClaimedTicketPresentation( app: BotApp, - ticket: { - channelId: string; - claimedBy: string | null; - claimedAt: number | null; - createdBy: string; - id: number; - type: string; - }, + ticket: Pick, ticketTypeName: string, claimerUsername: string ) { @@ -207,13 +219,7 @@ async function updateClaimedTicketPresentation( async function renameClaimedTicketChannel( app: BotApp, - ticket: { - channelId: string; - claimedBy: string | null; - createdBy: string; - id: number; - type: string; - }, + ticket: Pick, ticketTypeName: string, claimerUsername: string ) { diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index e295ad87..859a686f 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -26,7 +26,10 @@ import { createCustomId } from "@/core/custom-id"; import { editReply, reply, showModal } from "@/core/respond"; import type { BotApp, CommandExecutionContext, ComponentExecutionContext } from "@/core/types"; import { ticketsTable } from "@/db/schema"; +import { sendTicketLog } from "@/features/logs/service"; +import { createTicketLogContext } from "@/features/logs/utils"; import { getTicketType, hasTicketStaffAccess } from "@/features/tickets/config-access"; +import { DEFAULT_NO_REASON } from "@/features/tickets/constants"; import { appendMessageButton, finalizeMessageTemplate, @@ -80,6 +83,13 @@ export async function handleDeleteClosedTicketButton( content: "Deleting ticket channel...", flags: MessageFlags.Ephemeral }); + void sendTicketLog(context.app, { + kind: "ticketDelete", + actor: getInteractionUser(interaction), + reason: manageable.ticket.closedReason ?? DEFAULT_NO_REASON, + transcriptUrl: manageable.ticket.transcriptUrl, + ticket: createTicketLogContext(manageable.ticket, manageable.ticketType.name) + }); await context.app.client.api.channels.delete(channelId); } @@ -220,6 +230,13 @@ async function closeTicket( transcriptUrl: transcriptUrl ?? "", userId: ticket.createdBy }; + void sendTicketLog(app, { + kind: "ticketClose", + actor: closer, + reason: normalizedReason, + transcriptUrl, + ticket: createTicketLogContext(ticket, ticketType.name) + }); if (app.config.tickets.close.dmUserOnClose) { await status.update("Sending close confirmation..."); @@ -227,6 +244,13 @@ async function closeTicket( } if (app.config.tickets.close.deleteChannelOnClose) { + void sendTicketLog(app, { + kind: "ticketDelete", + actor: closer, + reason: normalizedReason, + transcriptUrl, + ticket: createTicketLogContext(ticket, ticketType.name) + }); await editReply(app, interaction, { content: transcriptUrl ? "Ticket closed. The transcript is ready and the channel will now be deleted." @@ -237,7 +261,10 @@ async function closeTicket( } await status.update("Posting close summary..."); - await app.client.api.channels.createMessage(ticket.channelId, await buildCloseChannelMessage(app, ticketType, closeMessageTokens)); + await app.client.api.channels.createMessage( + ticket.channelId, + await buildCloseChannelMessage(app, ticketType, closeMessageTokens) + ); await status.update("Ticket closed."); } @@ -428,13 +455,10 @@ async function buildCloseChannelMessage( } ) { const deleteButtonCustomId = createCustomId("tickets", "delete-closed"); - const messageTemplate = await loadMessageTemplate( - resolveCloseChannelMessageReference(app, ticketType), - { - ...tokens, - deleteButtonCustomId - } - ); + const messageTemplate = await loadMessageTemplate(resolveCloseChannelMessageReference(app, ticketType), { + ...tokens, + deleteButtonCustomId + }); return finalizeMessageTemplate( appendMessageButton( diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index 7371e67d..5e9495eb 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -34,6 +34,7 @@ import { createCustomId } from "@/core/custom-id"; import { deferReply, editReply, followUp, reply, showModal, updateMessage } from "@/core/respond"; import type { BotApp, ComponentExecutionContext } from "@/core/types"; import { type TicketRecord, ticketsTable } from "@/db/schema"; +import { sendTicketLog } from "@/features/logs/service"; import { getPanel, getPanelTicketTypeKeys, @@ -155,6 +156,7 @@ async function createTicket( const user = getInteractionUser(interaction); const ticketNumber = (await getNextTicketNumber(app)).toString(); + const createdAt = Date.now(); const channelName = renderChannelName(ticketType.channelNameTemplate ?? app.config.tickets.channelNameTemplate, { ticketNumber, ticketTypeKey, @@ -192,9 +194,22 @@ async function createTicket( type: ticketTypeKey, reason: serializeTicketOpenReason(reason), createdBy: user.id, - createdAt: Date.now(), + createdAt, invitedUserIds: "[]" }); + void sendTicketLog(app, { + kind: "ticketCreate", + actor: user, + reason: reason.combined, + ticket: { + ticketId: ticketNumber, + ticketChannelId: channel.id, + ticketTypeKey, + ticketTypeName: ticketType.name, + createdAt, + createdById: user.id + } + }); const successMessage = { content: `Your ticket has been created: <#${channel.id}>`, @@ -245,9 +260,7 @@ export async function buildTicketWelcomeMessage( closeButtonCustomId, staffMentions: roleMentions.length ? ` ${roleMentions.join(" ")}` : "" }; - const messageTemplate = messageReference - ? await loadMessageTemplate(messageReference, renderedTokens) - : {}; + const messageTemplate = messageReference ? await loadMessageTemplate(messageReference, renderedTokens) : {}; const configuredContent = ticketType.welcomeContent ?? app.config.tickets.defaultWelcomeContent; const runtimeText = configuredContent ? renderTemplate(configuredContent, renderedTokens) : undefined; const withRuntimeText = appendMessageText(messageTemplate, runtimeText, { slot: "runtime-text" }); From 30fce8f3cdbf6a46ca9e92c612b9f437e22cf9b7 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:13:35 +0200 Subject: [PATCH 25/67] refactor(messages): simplify text display components - Merge multiple text block strings into single components. - Utilize markdown subtext for close actions. - Condense padding in ticket opened messages. --- messages/logs/ticket-claimed.ts | 7 ++++--- messages/logs/ticket-closed.ts | 11 +++++------ messages/logs/ticket-created.ts | 9 +++++---- messages/logs/ticket-deleted.ts | 11 +++++------ messages/logs/ticket-renamed.ts | 9 +++++---- messages/logs/ticket-unclaimed.ts | 7 ++++--- messages/logs/user-added.ts | 6 ++++-- messages/logs/user-removed.ts | 6 ++++-- messages/tickets/ticket-closed-billing.ts | 8 ++------ messages/tickets/ticket-closed-dm-billing.ts | 8 ++------ messages/tickets/ticket-closed-dm-general.ts | 8 ++------ messages/tickets/ticket-closed-dm-report.ts | 8 ++------ messages/tickets/ticket-closed-dm.ts | 12 ++---------- messages/tickets/ticket-closed-general.ts | 8 ++------ messages/tickets/ticket-closed-report.ts | 8 ++------ messages/tickets/ticket-closed.ts | 12 ++---------- messages/tickets/ticket-opened-billing.ts | 2 +- messages/tickets/ticket-opened-general.ts | 2 +- messages/tickets/ticket-opened-report.ts | 2 +- messages/tickets/ticket-opened.ts | 2 +- 20 files changed, 56 insertions(+), 90 deletions(-) diff --git a/messages/logs/ticket-claimed.ts b/messages/logs/ticket-claimed.ts index bb94ae64..a90ada93 100644 --- a/messages/logs/ticket-claimed.ts +++ b/messages/logs/ticket-claimed.ts @@ -24,9 +24,10 @@ const ticketClaimedLogMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, content: "## Ticket Claimed" }, { type: ComponentType.TextDisplay, content: "{actorMention} claimed {ticketChannelMention}." }, - { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, - { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, - { type: ComponentType.TextDisplay, content: "**Open Age**\n{ticketAge}" } + { + type: ComponentType.TextDisplay, + content: "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Open Age**: {ticketAge}" + } ] } ] diff --git a/messages/logs/ticket-closed.ts b/messages/logs/ticket-closed.ts index 28ed4a0d..4a0ed8fd 100644 --- a/messages/logs/ticket-closed.ts +++ b/messages/logs/ticket-closed.ts @@ -24,12 +24,11 @@ const ticketClosedLogMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, content: "## Ticket Closed" }, { type: ComponentType.TextDisplay, content: "{actorMention} closed {ticketChannelMention}." }, - { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, - { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, - { type: ComponentType.TextDisplay, content: "**Claim Status**\n{claimStatus}" }, - { type: ComponentType.TextDisplay, content: "**Open Age**\n{ticketAge}" }, - { type: ComponentType.TextDisplay, content: "**Reason**\n{reason}" }, - { type: ComponentType.TextDisplay, content: "**Transcript**\n{transcriptStatus}" } + { + type: ComponentType.TextDisplay, + content: + "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Claim Status**: {claimStatus}\n**Open Age**: {ticketAge}\n**Reason**: {reason}\n**Transcript**: {transcriptStatus}" + } ] } ] diff --git a/messages/logs/ticket-created.ts b/messages/logs/ticket-created.ts index 3aec96a1..7b7cefcb 100644 --- a/messages/logs/ticket-created.ts +++ b/messages/logs/ticket-created.ts @@ -24,10 +24,11 @@ const ticketCreatedLogMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, content: "## Ticket Created" }, { type: ComponentType.TextDisplay, content: "{actorMention} opened {ticketChannelMention}." }, - { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, - { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, - { type: ComponentType.TextDisplay, content: "**Created**\n{createdAt}" }, - { type: ComponentType.TextDisplay, content: "**Reason**\n{reason}" } + { + type: ComponentType.TextDisplay, + content: + "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Created**: {createdAt}\n**Reason**: {reason}" + } ] } ] diff --git a/messages/logs/ticket-deleted.ts b/messages/logs/ticket-deleted.ts index c6db0b94..f389e533 100644 --- a/messages/logs/ticket-deleted.ts +++ b/messages/logs/ticket-deleted.ts @@ -24,12 +24,11 @@ const ticketDeletedLogMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, content: "## Ticket Deleted" }, { type: ComponentType.TextDisplay, content: "{actorMention} deleted {ticketChannelMention}." }, - { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, - { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, - { type: ComponentType.TextDisplay, content: "**Claim Status**\n{claimStatus}" }, - { type: ComponentType.TextDisplay, content: "**Open Age**\n{ticketAge}" }, - { type: ComponentType.TextDisplay, content: "**Close Reason**\n{reason}" }, - { type: ComponentType.TextDisplay, content: "**Transcript**\n{transcriptStatus}" } + { + type: ComponentType.TextDisplay, + content: + "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Claim Status**: {claimStatus}\n**Open Age**: {ticketAge}\n**Close Reason**: {reason}\n**Transcript**: {transcriptStatus}" + } ] } ] diff --git a/messages/logs/ticket-renamed.ts b/messages/logs/ticket-renamed.ts index 4e324091..6fe02ce5 100644 --- a/messages/logs/ticket-renamed.ts +++ b/messages/logs/ticket-renamed.ts @@ -24,10 +24,11 @@ const ticketRenamedLogMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, content: "## Ticket Renamed" }, { type: ComponentType.TextDisplay, content: "{actorMention} renamed {ticketChannelMention}." }, - { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, - { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, - { type: ComponentType.TextDisplay, content: "**From**\n`{oldChannelName}`" }, - { type: ComponentType.TextDisplay, content: "**To**\n`{newChannelName}`" } + { + type: ComponentType.TextDisplay, + content: + "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**From**: `{oldChannelName}`\n**To**: `{newChannelName}`" + } ] } ] diff --git a/messages/logs/ticket-unclaimed.ts b/messages/logs/ticket-unclaimed.ts index 88959116..2d9bced2 100644 --- a/messages/logs/ticket-unclaimed.ts +++ b/messages/logs/ticket-unclaimed.ts @@ -24,9 +24,10 @@ const ticketUnclaimedLogMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, content: "## Ticket Unclaimed" }, { type: ComponentType.TextDisplay, content: "{actorMention} unclaimed {ticketChannelMention}." }, - { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, - { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" }, - { type: ComponentType.TextDisplay, content: "**Open Age**\n{ticketAge}" } + { + type: ComponentType.TextDisplay, + content: "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Open Age**: {ticketAge}" + } ] } ] diff --git a/messages/logs/user-added.ts b/messages/logs/user-added.ts index b05d5865..ae439dbc 100644 --- a/messages/logs/user-added.ts +++ b/messages/logs/user-added.ts @@ -24,8 +24,10 @@ const userAddedLogMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, content: "## User Added" }, { type: ComponentType.TextDisplay, content: "{actorMention} added {targetMention} to {ticketChannelMention}." }, - { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, - { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" } + { + type: ComponentType.TextDisplay, + content: "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}" + } ] } ] diff --git a/messages/logs/user-removed.ts b/messages/logs/user-removed.ts index 861b8298..ce21e947 100644 --- a/messages/logs/user-removed.ts +++ b/messages/logs/user-removed.ts @@ -24,8 +24,10 @@ const userRemovedLogMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, content: "## User Removed" }, { type: ComponentType.TextDisplay, content: "{actorMention} removed {targetMention} from {ticketChannelMention}." }, - { type: ComponentType.TextDisplay, content: "**Ticket**\n#{ticketId} • {ticketTypeName}" }, - { type: ComponentType.TextDisplay, content: "**Opened By**\n{createdByMention}" } + { + type: ComponentType.TextDisplay, + content: "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}" + } ] } ] diff --git a/messages/tickets/ticket-closed-billing.ts b/messages/tickets/ticket-closed-billing.ts index 104c7e36..0673d51e 100644 --- a/messages/tickets/ticket-closed-billing.ts +++ b/messages/tickets/ticket-closed-billing.ts @@ -32,15 +32,11 @@ const billingTicketClosedMessage: LoadedMessageTemplate = { }, { type: ComponentType.TextDisplay, - content: "**Close Reason**\n{reason}" + content: "**Close Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" }, { type: ComponentType.TextDisplay, - content: "**Handled By**\n{closerMention}" - }, - { - type: ComponentType.TextDisplay, - content: "**Transcript**\n{transcriptStatus}" + content: "-# _Closed by {closerName}_" }, { type: ComponentType.ActionRow, diff --git a/messages/tickets/ticket-closed-dm-billing.ts b/messages/tickets/ticket-closed-dm-billing.ts index af59c414..b80f8171 100644 --- a/messages/tickets/ticket-closed-dm-billing.ts +++ b/messages/tickets/ticket-closed-dm-billing.ts @@ -32,15 +32,11 @@ const billingTicketClosedDmMessage: LoadedMessageTemplate = { }, { type: ComponentType.TextDisplay, - content: "**Reason**\n{reason}" + content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" }, { type: ComponentType.TextDisplay, - content: "**Handled By**\n{closerName}" - }, - { - type: ComponentType.TextDisplay, - content: "**Transcript**\n{transcriptStatus}" + content: "-# _Closed by {closerName}_" } ] } diff --git a/messages/tickets/ticket-closed-dm-general.ts b/messages/tickets/ticket-closed-dm-general.ts index 22e3a9bf..0bbe1539 100644 --- a/messages/tickets/ticket-closed-dm-general.ts +++ b/messages/tickets/ticket-closed-dm-general.ts @@ -28,15 +28,11 @@ const generalTicketClosedDmMessage: LoadedMessageTemplate = { }, { type: ComponentType.TextDisplay, - content: "**Reason**\n{reason}" + content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" }, { type: ComponentType.TextDisplay, - content: "**Handled By**\n{closerName}" - }, - { - type: ComponentType.TextDisplay, - content: "**Transcript**\n{transcriptStatus}" + content: "-# _Closed by {closerName}_" } ] } diff --git a/messages/tickets/ticket-closed-dm-report.ts b/messages/tickets/ticket-closed-dm-report.ts index 7a1eb1a7..dd266789 100644 --- a/messages/tickets/ticket-closed-dm-report.ts +++ b/messages/tickets/ticket-closed-dm-report.ts @@ -32,15 +32,11 @@ const reportTicketClosedDmMessage: LoadedMessageTemplate = { }, { type: ComponentType.TextDisplay, - content: "**Resolution Note**\n{reason}" + content: "**Resolution Note**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" }, { type: ComponentType.TextDisplay, - content: "**Handled By**\n{closerName}" - }, - { - type: ComponentType.TextDisplay, - content: "**Transcript**\n{transcriptStatus}" + content: "-# _Closed by {closerName}_" } ] } diff --git a/messages/tickets/ticket-closed-dm.ts b/messages/tickets/ticket-closed-dm.ts index f7932f86..efc0d894 100644 --- a/messages/tickets/ticket-closed-dm.ts +++ b/messages/tickets/ticket-closed-dm.ts @@ -28,19 +28,11 @@ const ticketClosedDmMessage: LoadedMessageTemplate = { }, { type: ComponentType.TextDisplay, - content: "**Reason**\n{reason}" + content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" }, { type: ComponentType.TextDisplay, - content: "**Claim**\n{claimStatus}" - }, - { - type: ComponentType.TextDisplay, - content: "**Transcript**\n{transcriptStatus}" - }, - { - type: ComponentType.TextDisplay, - content: "_Closed by {closerName}_" + content: "-# _Closed by {closerName}_" } ] } diff --git a/messages/tickets/ticket-closed-general.ts b/messages/tickets/ticket-closed-general.ts index a5141f0b..a005fc62 100644 --- a/messages/tickets/ticket-closed-general.ts +++ b/messages/tickets/ticket-closed-general.ts @@ -32,15 +32,11 @@ const generalTicketClosedMessage: LoadedMessageTemplate = { }, { type: ComponentType.TextDisplay, - content: "**Reason**\n{reason}" + content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" }, { type: ComponentType.TextDisplay, - content: "**Claim**\n{claimStatus}" - }, - { - type: ComponentType.TextDisplay, - content: "_Closed by {closerName}_" + content: "-# _Closed by {closerName}_" }, { type: ComponentType.ActionRow, diff --git a/messages/tickets/ticket-closed-report.ts b/messages/tickets/ticket-closed-report.ts index 538a7d2a..26d92f45 100644 --- a/messages/tickets/ticket-closed-report.ts +++ b/messages/tickets/ticket-closed-report.ts @@ -32,15 +32,11 @@ const reportTicketClosedMessage: LoadedMessageTemplate = { }, { type: ComponentType.TextDisplay, - content: "**Resolution Note**\n{reason}" + content: "**Resolution Note**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" }, { type: ComponentType.TextDisplay, - content: "**Claim**\n{claimStatus}" - }, - { - type: ComponentType.TextDisplay, - content: "**Transcript**\n{transcriptStatus}" + content: "-# _Closed by {closerName}_" }, { type: ComponentType.ActionRow, diff --git a/messages/tickets/ticket-closed.ts b/messages/tickets/ticket-closed.ts index 3e0ab210..b88fa80e 100644 --- a/messages/tickets/ticket-closed.ts +++ b/messages/tickets/ticket-closed.ts @@ -32,19 +32,11 @@ const ticketClosedMessage: LoadedMessageTemplate = { }, { type: ComponentType.TextDisplay, - content: "**Reason**\n{reason}" + content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" }, { type: ComponentType.TextDisplay, - content: "**Claim**\n{claimStatus}" - }, - { - type: ComponentType.TextDisplay, - content: "**Transcript**\n{transcriptStatus}" - }, - { - type: ComponentType.TextDisplay, - content: "_Closed by {closerName}_" + content: "-# _Closed by {closerName}_" }, { type: ComponentType.ActionRow, diff --git a/messages/tickets/ticket-opened-billing.ts b/messages/tickets/ticket-opened-billing.ts index b7d5766c..97d358fc 100644 --- a/messages/tickets/ticket-opened-billing.ts +++ b/messages/tickets/ticket-opened-billing.ts @@ -42,7 +42,7 @@ const billingTicketOpenedMessage: LoadedMessageTemplate = { createRuntimeTextSlot(), { type: ComponentType.TextDisplay, - content: "**Claim Status**\n{claimStatus}" + content: "**Claim Status**: {claimStatus}" }, createMessageSlot("actions") ] diff --git a/messages/tickets/ticket-opened-general.ts b/messages/tickets/ticket-opened-general.ts index ccd656f8..5fd4b735 100644 --- a/messages/tickets/ticket-opened-general.ts +++ b/messages/tickets/ticket-opened-general.ts @@ -42,7 +42,7 @@ const generalTicketOpenedMessage: LoadedMessageTemplate = { createRuntimeTextSlot(), { type: ComponentType.TextDisplay, - content: "**Claim Status**\n{claimStatus}" + content: "**Claim Status**: {claimStatus}" }, createMessageSlot("actions") ] diff --git a/messages/tickets/ticket-opened-report.ts b/messages/tickets/ticket-opened-report.ts index e233b7f8..b3c01686 100644 --- a/messages/tickets/ticket-opened-report.ts +++ b/messages/tickets/ticket-opened-report.ts @@ -42,7 +42,7 @@ const reportTicketOpenedMessage: LoadedMessageTemplate = { createRuntimeTextSlot(), { type: ComponentType.TextDisplay, - content: "**Claim Status**\n{claimStatus}" + content: "**Claim Status**: {claimStatus}" }, createMessageSlot("actions") ] diff --git a/messages/tickets/ticket-opened.ts b/messages/tickets/ticket-opened.ts index 7e197a16..d1cf27e6 100644 --- a/messages/tickets/ticket-opened.ts +++ b/messages/tickets/ticket-opened.ts @@ -42,7 +42,7 @@ const ticketOpenedMessage: LoadedMessageTemplate = { createRuntimeTextSlot(), { type: ComponentType.TextDisplay, - content: "**Claim Status**\n{claimStatus}" + content: "**Claim Status**: {claimStatus}" }, createMessageSlot("actions") ] From b332a828ecc7b4e0cdacf59d41d474f8f272b961 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:13:53 +0200 Subject: [PATCH 26/67] refactor(tickets): restructure message component traversal - Replace unsafe unknown recursive traversals with strict interfaces. - Support mapping over nested container structures for action rows. - Safely clone button arrays without mutating references. --- src/features/tickets/close-workflow.ts | 73 +++++++++++++++------- src/features/tickets/ticket-workflow.ts | 81 ++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 26 deletions(-) diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index 859a686f..424f5cbf 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -14,12 +14,14 @@ This notice must not be removed, obscured, or replaced. */ import type { + APIActionRowComponent, APIButtonComponentWithCustomId, APIChatInputApplicationCommandInteraction, APIMessage, APIMessageComponentInteraction, APIModalSubmitInteraction } from "@discordjs/core"; +import type { APIComponentInMessageActionRow, APIContainerComponent } from "discord-api-types/v10"; import { ButtonStyle, ComponentType, MessageFlags, TextInputStyle } from "@discordjs/core"; import { eq } from "drizzle-orm"; import { createCustomId } from "@/core/custom-id"; @@ -366,35 +368,60 @@ async function disableTicketActionButtons(app: BotApp, channelId: string, messag return; } - const nextComponents = message.components.map((row) => { - if (row.type !== ComponentType.ActionRow) { - return row; - } - - return { - ...row, - components: row.components.map((component) => { - if ( - component.type !== ComponentType.Button || - !("custom_id" in component) || - !disabledButtonIds.has(component.custom_id) - ) { - return component; - } - - return { - ...component, - disabled: true - }; - }) - }; - }) as APIMessage["components"]; + const nextComponents: APIMessage["components"] = disableNestedTicketActionButtons(message.components, disabledButtonIds); await app.client.api.channels.editMessage(channelId, messageId, { components: nextComponents }); } +function disableNestedTicketActionButtons( + components: NonNullable, + disabledButtonIds: Set +): NonNullable { + const nextComponents: NonNullable = components.map((component) => { + if (component.type === ComponentType.ActionRow) { + return disableTicketActionRow(component, disabledButtonIds); + } + + if (component.type === ComponentType.Container) { + return disableTicketActionContainer(component, disabledButtonIds); + } + + return component; + }); + + return nextComponents; +} + +function disableTicketActionContainer(container: APIContainerComponent, disabledButtonIds: Set): APIContainerComponent { + return { + ...container, + components: container.components.map((component) => + component.type === ComponentType.ActionRow ? disableTicketActionRow(component, disabledButtonIds) : component + ) + }; +} + +function disableTicketActionRow( + row: APIActionRowComponent, + disabledButtonIds: Set +): APIActionRowComponent { + return { + ...row, + components: row.components.map((component) => + isTicketActionButton(component, disabledButtonIds) ? { ...component, disabled: true } : component + ) as T[] + }; +} + +function isTicketActionButton( + component: APIComponentInMessageActionRow, + disabledButtonIds: Set +): component is APIButtonComponentWithCustomId { + return component.type === ComponentType.Button && "custom_id" in component && disabledButtonIds.has(component.custom_id); +} + async function moveClosedTicketChannel(app: BotApp, channelId: string) { const categoryId = app.config.tickets.close.closeTicketCategoryId?.trim(); diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index 5e9495eb..b26bf461 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -16,10 +16,12 @@ This notice must not be removed, obscured, or replaced. import type { APIActionRowComponent, APIButtonComponentWithCustomId, + APIMessageTopLevelComponent, APIMessageComponentInteraction, APIModalSubmitInteraction, APIModalSubmitTextInputComponent } from "@discordjs/core"; +import type { APIComponentInContainer, APIContainerComponent } from "discord-api-types/v10"; import { ButtonStyle, ChannelType, @@ -44,7 +46,6 @@ import { } from "@/features/tickets/config-access"; import { DEFAULT_NO_REASON, TICKET_ACCESS_ALLOW } from "@/features/tickets/constants"; import { - appendMessageComponents, appendMessageText, finalizeMessageTemplate, hasMessageComponentCustomId, @@ -271,7 +272,7 @@ export async function buildTicketWelcomeMessage( claimedBy: tokens.claimerId, disableActions: options?.disableActions ?? false }); - const body = appendMessageComponents(withRuntimeText, buttons, "actions"); + const body = attachWelcomeMessageActions(withRuntimeText, buttons); return finalizeMessageTemplate({ ...body, @@ -335,7 +336,7 @@ function buildTicketActionButtons( claimedBy?: string; disableActions: boolean; } -) { +): TicketActionRows | undefined { const buttons: APIButtonComponentWithCustomId[] = []; if (app.config.tickets.close.showCloseButton && !hasMessageComponentCustomId(payload, options.closeButtonCustomId)) { @@ -382,6 +383,80 @@ function buildTicketActionButtons( ]; } +function attachWelcomeMessageActions( + payload: LoadedMessageTemplate, + components: TicketActionRows | undefined +): LoadedMessageTemplate { + if (!components?.length || !payload.components?.length) { + return payload; + } + + let attached = false; + const nextComponents = payload.components.map((component) => { + if (attached || component.type !== ComponentType.Container) { + return component; + } + + attached = true; + return attachActionsToWelcomeContainer(component as WelcomeTemplateContainer, components) as APIMessageTopLevelComponent; + }); + + return { + ...payload, + components: attached ? nextComponents : [...payload.components, ...cloneActionRows(components)] + }; +} + +function attachActionsToWelcomeContainer( + container: WelcomeTemplateContainer, + actions: TicketActionRows +): WelcomeTemplateContainer { + const nextComponents: WelcomeTemplateContainer["components"] = []; + let replacedSlot = false; + + for (const component of container.components) { + if (isActionSlot(component)) { + nextComponents.push(...cloneActionRows(actions)); + replacedSlot = true; + continue; + } + + nextComponents.push(component); + } + + if (!replacedSlot) { + nextComponents.push(...cloneActionRows(actions)); + } + + return { + ...container, + components: nextComponents + }; +} + +function cloneActionRows(components: TicketActionRows): TicketActionRows { + return components.map((component) => ({ + ...component, + components: [...component.components] + })); +} + +function isActionSlot(value: WelcomeTemplateComponent): value is TemplateSlotComponent { + return "slot" in value && value.slot === "actions"; +} + +type TemplateSlotComponent = { + slot: string; + slot_kind?: string; + type?: string; +}; + +type WelcomeTemplateComponent = APIComponentInContainer | TemplateSlotComponent; +type WelcomeTemplateContainer = Omit & { + components: WelcomeTemplateComponent[]; +}; +type TicketActionRows = APIActionRowComponent[]; + function createQuestionInput(question: TicketQuestionConfig) { return { type: ComponentType.TextInput, From fa7f1184615310a34586b80fa098d22274ec88e5 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:24:54 +0200 Subject: [PATCH 27/67] chore(workspace): add DOM lib and dictionary words * Include DOM and DOM.Iterable in tsconfig lib for WebSocket support * Add ticketbot to cSpell workspace dictionary --- .vscode/settings.json | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 39eb4509..0bbb376e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["bunx", "cleardm", "libsql", "replyable", "ticketpm", "tsgo", "typesafe"] + "cSpell.words": ["bunx", "cleardm", "libsql", "replyable", "ticketbot", "ticketpm", "tsgo", "typesafe"] } diff --git a/tsconfig.json b/tsconfig.json index 8cf2c935..f2267e8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { // Enable latest features - "lib": ["ESNext"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", From 8863806a5cd2be9ac1e93beab448043ddf1c1f4c Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:25:09 +0200 Subject: [PATCH 28/67] feat(telemetry): add anonymous tracking service * Introduce telemetry connection via WebSocket on ticket.pm * Store privacy notice state in new appMetaTable * Add minimalTracking and showWSLog configuration variables * Trigger telemetry start in client ready event --- config/config.example.ts | 4 + src/config/index.ts | 4 + src/db/schema.ts | 7 + src/events/ready.ts | 3 + src/telemetry.ts | 278 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 296 insertions(+) create mode 100644 src/telemetry.ts diff --git a/config/config.example.ts b/config/config.example.ts index 660a4e30..63b2511d 100644 --- a/config/config.example.ts +++ b/config/config.example.ts @@ -24,6 +24,10 @@ export default defineConfig("0.0.1", { // Transcript ID style used by ticket.pm uploads. // "uuid" matches the current default. "emoji" keeps the older style. uuidType: "uuid", + // Reduce telemetry to the bot version and runtime version only. + minimalTracking: false, + // Log websocket connect, close, and error events for telemetry. + showWSLog: false, logs: { // Set to true to post audit logs for ticket actions. enabled: true, diff --git a/src/config/index.ts b/src/config/index.ts index 50ba07d6..b9573a54 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -22,6 +22,10 @@ interface ConfigV0_0_1 { lang: "en"; /** Controls the transcript ID style when uploading to ticket.pm */ uuidType?: "uuid" | "emoji"; + /** Reduce telemetry to bot/runtime version only */ + minimalTracking?: boolean; + /** Enable websocket lifecycle logs for telemetry */ + showWSLog?: boolean; logs: { enabled: boolean; channelId: string; diff --git a/src/db/schema.ts b/src/db/schema.ts index f9d7e04a..01d35523 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -24,6 +24,12 @@ export const panelMessagesTable = sqliteTable("panel_messages", { updatedAt: int().notNull() }); +export const appMetaTable = sqliteTable("app_meta", { + key: text().primaryKey(), + value: text().notNull(), + updatedAt: int().notNull() +}); + export const ticketsTable = sqliteTable("tickets", { id: int().primaryKey({ autoIncrement: true }), /** The ID of the channel where the ticket was created. */ @@ -49,6 +55,7 @@ export const ticketsTable = sqliteTable("tickets", { transcriptUrl: text() }); +export type AppMetaRecord = typeof appMetaTable.$inferSelect; export type TicketRecord = typeof ticketsTable.$inferSelect; /* diff --git a/src/events/ready.ts b/src/events/ready.ts index dc66d9db..5b895a24 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -25,6 +25,7 @@ import { defineEvent } from "@/core/defineEvent"; import type { BotApp } from "@/core/types"; import { deployApplicationCommands } from "@/deploy-commands"; import { syncTicketPanels } from "@/features/tickets/service"; +import { announceTelemetryPrivacy, startTelemetry } from "@/telemetry"; const PRESENCE_REFRESH_INTERVAL_MS = 900_000; const SPONSORS_URL = "https://raw.githubusercontent.com/Sayrix/sponsors/main/sponsors.json"; @@ -72,6 +73,8 @@ const readyEvent = defineEvent<[ToEventProps]>({ void applyConfiguredPresence(app); }, PRESENCE_REFRESH_INTERVAL_MS); await announceStartup(app, `${event.data.user.username}#${event.data.user.discriminator}`, event.data.user.id); + await announceTelemetryPrivacy(app); + startTelemetry(app, event.data.guilds.length); } }); diff --git a/src/telemetry.ts b/src/telemetry.ts new file mode 100644 index 00000000..8f7efb76 --- /dev/null +++ b/src/telemetry.ts @@ -0,0 +1,278 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import os from "node:os"; +import { eq } from "drizzle-orm"; +import type { BotApp } from "@/core/types"; +import { appMetaTable } from "@/db/schema"; +import { BOT_VERSION } from "@/version"; + +const TELEMETRY_SOCKET_URL = "wss://telemetry.ticket.pm"; +// const TELEMETRY_SOCKET_URL = "ws://localhost:45263"; +const TELEMETRY_PROTOCOL_VERSION = "v1"; +const TELEMETRY_SOCKET_PROTOCOL = `ticket.pm.telemetry.${TELEMETRY_PROTOCOL_VERSION}`; +const TELEMETRY_SEND_INTERVAL_MS = 300_000; // 5 minutes +const TELEMETRY_RECONNECT_MAX_DELAY_MS = 10_000; +const TELEMETRY_NOTICE_KEY = "telemetryPrivacyNoticeShown"; + +export async function announceTelemetryPrivacy(app: BotApp) { + const existingRows = await app.db.select().from(appMetaTable).where(eq(appMetaTable.key, TELEMETRY_NOTICE_KEY)).limit(1); + + if (existingRows[0]) { + return; + } + + if (isMinimalTrackingEnabled(app)) { + app.logger.warn( + ` +PRIVACY NOTICE +------------------------------- +Minimal tracking is enabled; the following information is sent anonymously: +* Current source version +* Runtime version +-------------------------------`.trim() + ); + } else { + app.logger.warn( + ` +PRIVACY NOTICE +------------------------------- +Telemetry is currently set to full and the following information is sent anonymously: +* Discord bot guild count and user count +* Current source version +* Runtime version +* OS version +* CPU model, core count, and architecture +* Current process uptime +* System total RAM and free RAM +------------------------------- +If you wish to minimize the information that is sent, set "minimalTracking" to true in the config.`.trim() + ); + } + + await app.db + .insert(appMetaTable) + .values({ + key: TELEMETRY_NOTICE_KEY, + value: "true", + updatedAt: Date.now() + }) + .onConflictDoUpdate({ + target: appMetaTable.key, + set: { + value: "true", + updatedAt: Date.now() + } + }); +} + +export function startTelemetry(app: BotApp, guildCount: number) { + const enableLog = shouldShowWSLog(app); + let socket: WebSocket | null = null; + let reconnectTimeout: ReturnType | null = null; + let sendInterval: ReturnType | null = null; + let stopped = false; + + const clearReconnectTimeout = () => { + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeout = null; + } + }; + + const clearSendInterval = () => { + if (sendInterval) { + clearInterval(sendInterval); + sendInterval = null; + } + }; + + const logSocketEvent = (message: string, error?: unknown) => { + if (!enableLog) { + return; + } + + if (error) { + app.logger.warn(message, error); + return; + } + + app.logger.info(message); + }; + + const scheduleReconnect = () => { + if (stopped || reconnectTimeout) { + return; + } + + reconnectTimeout = setTimeout(() => { + reconnectTimeout = null; + connect(); + // We add a random delay to the reconnect to avoid thundering herd problem if the telemetry server is down. + }, Math.random() * TELEMETRY_RECONNECT_MAX_DELAY_MS); + }; + + const cleanupSocket = (instance: WebSocket | null) => { + if (socket === instance) { + socket = null; + } + + clearSendInterval(); + }; + + const sendTelemetry = async (instance: WebSocket) => { + if (instance !== socket || instance.readyState !== WebSocket.OPEN) { + return; + } + + try { + instance.send(JSON.stringify(await buildTelemetryPayload(app, guildCount))); + } catch (error) { + logSocketEvent("Telemetry websocket send failed.", error); + } + }; + + const connect = () => { + if (stopped || socket) { + return; + } + + clearReconnectTimeout(); + const nextSocket = new WebSocket(TELEMETRY_SOCKET_URL, TELEMETRY_SOCKET_PROTOCOL); + socket = nextSocket; + + nextSocket.addEventListener("open", () => { + if (socket !== nextSocket) { + nextSocket.close(); + return; + } + + logSocketEvent("Connected to telemetry websocket."); + void sendTelemetry(nextSocket); + + clearSendInterval(); + sendInterval = setInterval(() => { + void sendTelemetry(nextSocket); + }, TELEMETRY_SEND_INTERVAL_MS); + }); + + nextSocket.addEventListener("error", (event: Event) => { + logSocketEvent("Telemetry websocket error.", event); + cleanupSocket(nextSocket); + scheduleReconnect(); + }); + + nextSocket.addEventListener("close", () => { + logSocketEvent("Telemetry websocket closed."); + cleanupSocket(nextSocket); + scheduleReconnect(); + }); + }; + + connect(); + + return () => { + stopped = true; + clearReconnectTimeout(); + clearSendInterval(); + socket?.close(); + socket = null; + }; +} + +async function buildTelemetryPayload(app: BotApp, guildCount: number) { + const runtime = getRuntimeInfo(); + + if (isMinimalTrackingEnabled(app)) { + return { + type: "telemetry", + data: { + infos: { + ticketbotVersion: BOT_VERSION, + runtimeName: runtime.name, + runtimeVersion: runtime.version + } + } + }; + } + + const guild = await app.client.api.guilds.get(app.config.guildId, { with_counts: true }).catch(() => null); + const cpuInfo = os.cpus()[0]; + + return { + type: "telemetry", + data: { + stats: { + guilds: guildCount, + users: guild?.approximate_member_count ?? 0 + }, + infos: { + ticketbotVersion: BOT_VERSION, + runtimeName: runtime.name, + runtimeVersion: runtime.version, + os: os.platform(), + osVersion1: os.release(), + osVersion2: os.version(), + uptime: process.uptime(), + ram: { + total: os.totalmem(), + free: os.freemem() + }, + cpu: { + model: cpuInfo?.model ?? "unknown", + cores: os.cpus().length, + arch: os.arch() + } + } + } + }; +} + +function isMinimalTrackingEnabled(app: BotApp) { + return app.config.minimalTracking ?? false; +} + +function shouldShowWSLog(app: BotApp) { + return app.config.showWSLog ?? false; +} + +function getRuntimeInfo() { + if (typeof process.versions.bun === "string") { + return { + name: "bun", + version: process.versions.bun + } as const; + } + + return { + name: "node", + version: process.versions.node + } as const; +} + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ From aaaa8d8963e093d28bab9501a9c53618e238c323 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:38:24 +0200 Subject: [PATCH 29/67] refactor(tickets): optimize ticket closing process and improve permission handling --- src/features/tickets/close-workflow.ts | 116 +++++++++++++++++++------ 1 file changed, 88 insertions(+), 28 deletions(-) diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index 424f5cbf..1891fea1 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -21,8 +21,8 @@ import type { APIMessageComponentInteraction, APIModalSubmitInteraction } from "@discordjs/core"; -import type { APIComponentInMessageActionRow, APIContainerComponent } from "discord-api-types/v10"; -import { ButtonStyle, ComponentType, MessageFlags, TextInputStyle } from "@discordjs/core"; +import { ButtonStyle, ComponentType, MessageFlags, OverwriteType, TextInputStyle } from "@discordjs/core"; +import type { APIComponentInMessageActionRow, APIContainerComponent, RESTAPIChannelPatchOverwrite } from "discord-api-types/v10"; import { eq } from "drizzle-orm"; import { createCustomId } from "@/core/custom-id"; import { editReply, reply, showModal } from "@/core/respond"; @@ -192,20 +192,14 @@ async function closeTicket( .where(eq(ticketsTable.channelId, ticket.channelId)); if (!app.config.tickets.close.deleteChannelOnClose) { - await status.update("Updating ticket access..."); - await revokeTicketParticipantAccess(app, ticket.channelId, ticket.createdBy); - - for (const invitedUserId of getInvitedUserIds(ticket)) { - await revokeTicketParticipantAccess(app, ticket.channelId, invitedUserId); - } - } - - // Keep the original ticket message in sync with the closed state when the - // channel is preserved for staff review. - await disableTicketActionButtons(app, ticket.channelId, ticket.creationMessageId); + const invitedUserIds = getInvitedUserIds(ticket); - if (!app.config.tickets.close.deleteChannelOnClose) { - await moveClosedTicketChannel(app, ticket.channelId); + runCloseTaskInBackground(app, ticket.channelId, "disable ticket actions", async () => { + // Preserve the original ticket message while preventing new actions. + await disableTicketActionButtons(app, ticket.channelId, ticket.creationMessageId); + }); + await status.update("Updating ticket access..."); + await removeClosedTicketParticipantAccess(app, ticket.channelId, ticket.createdBy, invitedUserIds); } const transcriptJob = app.config.tickets.close.createTranscript @@ -213,6 +207,13 @@ async function closeTicket( onStatus: (content) => status.update(content) }) : null; + + if (!app.config.tickets.close.deleteChannelOnClose) { + runCloseTaskInBackground(app, ticket.channelId, "move the closed ticket channel", async () => { + await moveClosedTicketChannel(app, ticket.channelId); + }); + } + const transcriptUrl = transcriptJob ? await transcriptJob.waitForResult() : null; if (app.config.tickets.close.createTranscript && !transcriptUrl) { @@ -240,7 +241,7 @@ async function closeTicket( ticket: createTicketLogContext(ticket, ticketType.name) }); - if (app.config.tickets.close.dmUserOnClose) { + if (app.config.tickets.close.deleteChannelOnClose && app.config.tickets.close.dmUserOnClose) { await status.update("Sending close confirmation..."); await sendCloseDm(app, ticket.createdBy, ticketType, closeMessageTokens); } @@ -262,11 +263,15 @@ async function closeTicket( return; } - await status.update("Posting close summary..."); - await app.client.api.channels.createMessage( - ticket.channelId, - await buildCloseChannelMessage(app, ticketType, closeMessageTokens) - ); + const closeSummaryMessage = await buildCloseChannelMessage(app, ticketType, closeMessageTokens); + const closeTasks: Promise[] = [app.client.api.channels.createMessage(ticket.channelId, closeSummaryMessage)]; + + if (app.config.tickets.close.dmUserOnClose) { + closeTasks.push(sendCloseDm(app, ticket.createdBy, ticketType, closeMessageTokens)); + } + + await status.update(app.config.tickets.close.dmUserOnClose ? "Sending close updates..." : "Posting close summary..."); + await Promise.all(closeTasks); await status.update("Ticket closed."); } @@ -434,6 +439,39 @@ async function moveClosedTicketChannel(app: BotApp, channelId: string) { }); } +async function removeClosedTicketParticipantAccess(app: BotApp, channelId: string, openerId: string, invitedUserIds: string[]) { + if (invitedUserIds.length === 0) { + await revokeTicketParticipantAccess(app, channelId, openerId); + return; + } + + const channel = await app.client.api.channels.get(channelId); + + if (!("permission_overwrites" in channel)) { + return; + } + + const removedUserIds = new Set([openerId, ...invitedUserIds]); + const nextOverwrites: RESTAPIChannelPatchOverwrite[] = (channel.permission_overwrites ?? []) + .filter((overwrite) => overwrite.type !== OverwriteType.Member || !removedUserIds.has(overwrite.id)) + .map((overwrite) => ({ + id: overwrite.id, + type: overwrite.type, + allow: overwrite.allow ?? "0", + deny: overwrite.deny ?? "0" + })); + + await app.client.api.channels.edit(channelId, { + permission_overwrites: nextOverwrites + }); +} + +function runCloseTaskInBackground(app: BotApp, channelId: string, action: string, task: () => Promise) { + void task().catch((error) => { + app.logger.error(`Failed to ${action} for ticket channel ${channelId}.`, error); + }); +} + async function sendCloseDm( app: BotApp, userId: string, @@ -515,26 +553,48 @@ function createCloseStatusUpdater( interaction: APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction | APIModalSubmitInteraction ) { let hasStarted = false; - let lastContent = ""; + let displayedContent = ""; + let requestedContent = ""; + let flushPromise: Promise | null = null; + + const flushLatestContent = async (): Promise => { + while (hasStarted && requestedContent !== displayedContent) { + if (!flushPromise) { + const nextContent = requestedContent; + + flushPromise = editReply(app, interaction, { + content: nextContent + }) + .catch(() => undefined) + .then(() => { + displayedContent = nextContent; + }) + .finally(() => { + flushPromise = null; + }); + } + + await flushPromise; + } + }; return { start: async (content: string) => { hasStarted = true; - lastContent = content; + displayedContent = content; + requestedContent = content; await reply(app, interaction, { content, flags: MessageFlags.Ephemeral }); }, update: async (content: string) => { - if (!hasStarted || content === lastContent) { + if (!hasStarted || content === requestedContent) { return; } - lastContent = content; - await editReply(app, interaction, { - content - }).catch(() => undefined); + requestedContent = content; + await flushLatestContent(); } }; } From dbc07794bbfff391055aceb9a457bc37f5d1add5 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:08:41 +0200 Subject: [PATCH 30/67] feat(core): implement i18n localization system * Add typesafe-i18n configuration and generated locales * Implement BotApp locale context injection * Expand config schemas to accommodate language settings * Update core registries and interaction responders to use locales --- .typesafe-i18n.json | 2 +- config/config.example.ts | 1 + i18n/en/index.ts | 296 ++++++ i18n/formatters.ts | 6 + i18n/fr/index.ts | 294 ++++++ i18n/i18n-types.ts | 1850 ++++++++++++++++++++++++++++++++++++++ i18n/i18n-util.async.ts | 26 + i18n/i18n-util.sync.ts | 25 + i18n/i18n-util.ts | 37 + src/app.ts | 6 +- src/config/index.ts | 4 +- src/core/discovery.ts | 5 +- src/core/i18n.ts | 29 + src/core/registry.ts | 20 +- src/core/respond.ts | 2 +- src/core/types.ts | 9 +- 16 files changed, 2598 insertions(+), 14 deletions(-) create mode 100644 i18n/en/index.ts create mode 100644 i18n/formatters.ts create mode 100644 i18n/fr/index.ts create mode 100644 i18n/i18n-types.ts create mode 100644 i18n/i18n-util.async.ts create mode 100644 i18n/i18n-util.sync.ts create mode 100644 i18n/i18n-util.ts create mode 100644 src/core/i18n.ts diff --git a/.typesafe-i18n.json b/.typesafe-i18n.json index fa38040c..eef6ee59 100644 --- a/.typesafe-i18n.json +++ b/.typesafe-i18n.json @@ -4,5 +4,5 @@ "baseLocale": "en", "outputPath": "./i18n", "esmImports": true, - "banner": "// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten." + "banner": "// biome-ignore-all lint: autogenerated file" } diff --git a/config/config.example.ts b/config/config.example.ts index 63b2511d..df77bc33 100644 --- a/config/config.example.ts +++ b/config/config.example.ts @@ -20,6 +20,7 @@ export default defineConfig("0.0.1", { clientId: "123456789012345678", // The guild where the bot is installed and where commands should be deployed. guildId: "123456789012345678", + // Supported locales: "en", "fr" lang: "en", // Transcript ID style used by ticket.pm uploads. // "uuid" matches the current default. "emoji" keeps the older style. diff --git a/i18n/en/index.ts b/i18n/en/index.ts new file mode 100644 index 00000000..103fef81 --- /dev/null +++ b/i18n/en/index.ts @@ -0,0 +1,296 @@ +import type { BaseTranslation } from "../i18n-types.js"; + +const en: BaseTranslation = { + shared: { + unexpected_interaction_error: "An unexpected error occurred while handling this interaction.", + no_reason_provided: "No additional details were provided.", + claim_status: { + claimed_by: "Claimed by <@{userId:string}>", + unclaimed: "Unclaimed" + }, + transcript_status: { + ready: "[Open Transcript]({url:string})", + unavailable: "Unavailable or still processing." + } + }, + commands: { + add: { + description: "Add someone to the current ticket", + options: { + user: { + description: "The user to add" + } + }, + choose_user: "Choose a user to add to this ticket.", + already_has_access: "That user already has access to this ticket.", + already_invited: "That user is already invited to this ticket.", + invite_limit_reached: "You cannot invite more than {limit:number} users to one ticket.", + success: "Added <@{userId:string}> to this ticket." + }, + claim: { + description: "Claim the current ticket", + disabled: "Ticket claiming is disabled.", + already_claimed: "You already claimed this ticket.", + cannot_take_over: "This ticket is already claimed and cannot be taken over.", + only_staff: "Only staff can claim this ticket.", + success: "You claimed this ticket.", + reassigned: "Ticket reassigned to <@{userId:string}>." + }, + close: { + description: "Close the current ticket" + }, + cleardm: { + description: "Clear the bot's ticket history from your DMs", + starting: "Clearing your ticket DM history...", + dm_unavailable: "I could not access your DM channel.", + cleared: "Cleared {count:number} ticket DM messages.", + none_found: "No ticket DM messages were found." + }, + mass_add: { + description: "Add multiple users to the current ticket", + options: { + users: { + description: "Comma-separated user IDs or mentions" + } + }, + provide_users: "Provide at least one user ID or mention.", + summary: { + added: "Added {mentions:string}.", + none_added: "No users were added.", + skipped_existing: "Skipped {count:number} user(s) that already had access.", + skipped_invalid: "Skipped {count:number} invalid user ID(s).", + limit_reached: "Stopped when the {limit:number}-user ticket limit was reached." + } + }, + remove: { + description: "Remove invited users from the current ticket", + options: { + user: { + description: "The invited user to remove immediately" + } + }, + no_invited_users: "There are no invited users to remove from this ticket.", + select_users: "Select the invited users you want to remove from this ticket.", + select_placeholder: "Choose users to remove", + not_invited: "Those users are not invited to this ticket.", + success: "Removed {mentions:string} from this ticket." + }, + rename: { + description: "Rename the current ticket", + options: { + name: { + description: "The new ticket channel name" + } + }, + only_staff: "Only staff can rename this ticket.", + provide_name: "Provide a new ticket name.", + success: "Ticket renamed to <#{channelId:string}>." + }, + unclaim: { + description: "Unclaim the current ticket", + disabled: "Unclaiming is disabled for this server.", + not_claimed: "This ticket is not claimed.", + only_current_claimer: "Only the current claimer can unclaim this ticket.", + success: "You unclaimed this ticket." + } + }, + tickets: { + records: { + not_ticket_channel: "This interaction was not used in a ticket channel.", + not_open_ticket: "This channel is not an open ticket.", + already_closed: "This ticket is already closed." + }, + panel: { + no_visible_types: "You do not have access to any ticket types on this panel.", + select_type: "Please select a ticket type.", + unavailable_type: "That ticket type is not available from this panel.", + select_placeholder: "Select a ticket type" + }, + open: { + not_allowed_type: "You are not allowed to create that ticket type.", + unavailable_type: "That ticket type is not available from this panel.", + max_open_reached: "You already have the maximum number of open tickets ({limit:number}).", + created: "Your ticket has been created: <#{channelId:string}>", + question_answer: "{label:string}: {answer:string}" + }, + claim: { + only_staff: "Only staff can claim this ticket." + }, + actions: { + close_ticket: "Close Ticket", + claim_ticket: "Claim Ticket", + unclaim_ticket: "Unclaim Ticket", + delete_ticket: "Delete Ticket" + }, + close: { + delete_channel_start: "Deleting ticket channel...", + modal: { + title: "Close Ticket", + reason_label: "Reason", + reason_placeholder: "Why is this ticket being closed?" + }, + status: { + preparing_transcript: "Preparing transcript...", + closing_ticket: "Closing ticket...", + updating_access: "Updating ticket access...", + transcript_still_processing: "Transcript is still processing. Finishing ticket close...", + sending_close_confirmation: "Sending close confirmation...", + sending_close_updates: "Sending close updates...", + posting_close_summary: "Posting close summary...", + closed: "Ticket closed." + }, + deleted_with_transcript: "Ticket closed. The transcript is ready and the channel will now be deleted.", + deleted_without_transcript: "Ticket closed. The channel will now be deleted.", + only_staff: "Only staff can close this ticket.", + must_be_claimed: "This ticket must be claimed before it can be closed.", + only_current_claimer: "Only the current claimer can close this ticket.", + not_ticket: "This channel is not a ticket.", + only_closed_delete: "Only closed tickets can be deleted from this button.", + only_staff_delete: "Only staff can delete this ticket." + }, + transcript: { + collecting_messages: "Collecting ticket messages...", + creating: "Creating transcript...", + uploading: "Uploading transcript...", + uploading_avatars: "Uploading avatars...", + uploading_attachments: "Uploading attachments...", + progress: "{label:string} ({completed:number}/{total:number})" + }, + templates: { + open_panel: { + title: "## Open a Ticket", + description: "Choose the category that matches your request and the bot will create a private ticket for you." + }, + ticket_opened: { + title: "## {ticketTypeName:string} Ticket", + intro: "Thanks for opening a ticket.", + details_label: "**Details**\n{reason:string}", + claim_status: "**Claim Status**: {claimStatus:string}" + }, + ticket_opened_general: { + title: "## General Support Ticket", + intro: "A support team member will review this request soon.", + details_label: "**Summary**\n{reason:string}", + claim_status: "**Claim Status**: {claimStatus:string}" + }, + ticket_opened_billing: { + title: "## Billing Ticket", + intro: "Include invoice numbers, payment method, and any failed transaction details.", + details_label: "**Submitted Details**\n{reason:string}", + claim_status: "**Claim Status**: {claimStatus:string}" + }, + ticket_opened_report: { + title: "## Report Ticket", + intro: "Moderation staff will review the report and any evidence attached.", + details_label: "**Report Details**\n{reason:string}", + claim_status: "**Claim Status**: {claimStatus:string}" + }, + ticket_closed: { + title: "## Ticket Closed", + subtitle: "<@{userId:string}>'s ticket has been closed.", + details: "**Reason**: {reason:string}\n**Claim**: {claimStatus:string}\n**Transcript**: {transcriptStatus:string}", + closed_by: "-# _Closed by {closerName:string}_" + }, + ticket_closed_general: { + title: "## General Support Closed", + subtitle: "<@{userId:string}>'s general support ticket is now closed.", + details: "**Reason**: {reason:string}\n**Claim**: {claimStatus:string}\n**Transcript**: {transcriptStatus:string}", + closed_by: "-# _Closed by {closerName:string}_" + }, + ticket_closed_billing: { + title: "## Billing Ticket Closed", + subtitle: "<@{userId:string}>'s billing ticket has been closed.", + details: "**Close Reason**: {reason:string}\n**Claim**: {claimStatus:string}\n**Transcript**: {transcriptStatus:string}", + closed_by: "-# _Closed by {closerName:string}_" + }, + ticket_closed_report: { + title: "## Report Case Closed", + subtitle: "The report opened by <@{userId:string}> has been closed.", + details: + "**Resolution Note**: {reason:string}\n**Claim**: {claimStatus:string}\n**Transcript**: {transcriptStatus:string}", + closed_by: "-# _Closed by {closerName:string}_" + }, + ticket_closed_dm: { + title: "## Your ticket has been closed", + details: "**Reason**: {reason:string}\n**Claim**: {claimStatus:string}\n**Transcript**: {transcriptStatus:string}", + closed_by: "-# _Closed by {closerName:string}_" + }, + ticket_closed_dm_general: { + title: "## Your general support ticket has been closed", + details: "**Reason**: {reason:string}\n**Claim**: {claimStatus:string}\n**Transcript**: {transcriptStatus:string}", + closed_by: "-# _Closed by {closerName:string}_" + }, + ticket_closed_dm_billing: { + title: "## Your billing ticket has been closed", + intro: "If you still need help, open a new billing ticket and include your order details again.", + details: "**Reason**: {reason:string}\n**Claim**: {claimStatus:string}\n**Transcript**: {transcriptStatus:string}", + closed_by: "-# _Closed by {closerName:string}_" + }, + ticket_closed_dm_report: { + title: "## Your report ticket has been closed", + intro: "Staff reviewed the report and any attached evidence.", + details: + "**Resolution Note**: {reason:string}\n**Claim**: {claimStatus:string}\n**Transcript**: {transcriptStatus:string}", + closed_by: "-# _Closed by {closerName:string}_" + } + } + }, + logs: { + duration: { + day_short: "d", + hour_short: "h", + minute_short: "m", + second_short: "s" + }, + templates: { + ticket_created: { + title: "## Ticket Created", + action: "{actorMention:string} opened {ticketChannelMention:string}.", + details: + "**Ticket**: #{ticketId:string} - {ticketTypeName:string}\n**Opened By**: {createdByMention:string}\n**Created**: {createdAt:string}\n**Reason**: {reason:string}" + }, + ticket_claimed: { + title: "## Ticket Claimed", + action: "{actorMention:string} claimed {ticketChannelMention:string}.", + details: + "**Ticket**: #{ticketId:string} - {ticketTypeName:string}\n**Opened By**: {createdByMention:string}\n**Open Age**: {ticketAge:string}" + }, + ticket_unclaimed: { + title: "## Ticket Unclaimed", + action: "{actorMention:string} unclaimed {ticketChannelMention:string}.", + details: + "**Ticket**: #{ticketId:string} - {ticketTypeName:string}\n**Opened By**: {createdByMention:string}\n**Open Age**: {ticketAge:string}" + }, + ticket_closed: { + title: "## Ticket Closed", + action: "{actorMention:string} closed {ticketChannelMention:string}.", + details: + "**Ticket**: #{ticketId:string} - {ticketTypeName:string}\n**Opened By**: {createdByMention:string}\n**Claim Status**: {claimStatus:string}\n**Open Age**: {ticketAge:string}\n**Reason**: {reason:string}\n**Transcript**: {transcriptStatus:string}" + }, + ticket_deleted: { + title: "## Ticket Deleted", + action: "{actorMention:string} deleted {ticketChannelMention:string}.", + details: + "**Ticket**: #{ticketId:string} - {ticketTypeName:string}\n**Opened By**: {createdByMention:string}\n**Claim Status**: {claimStatus:string}\n**Open Age**: {ticketAge:string}\n**Close Reason**: {reason:string}\n**Transcript**: {transcriptStatus:string}" + }, + ticket_renamed: { + title: "## Ticket Renamed", + action: "{actorMention:string} renamed {ticketChannelMention:string}.", + details: + "**Ticket**: #{ticketId:string} - {ticketTypeName:string}\n**Opened By**: {createdByMention:string}\n**From**: `{oldChannelName:string}`\n**To**: `{newChannelName:string}`" + }, + user_added: { + title: "## User Added", + action: "{actorMention:string} added {targetMention:string} to {ticketChannelMention:string}.", + details: "**Ticket**: #{ticketId:string} - {ticketTypeName:string}\n**Opened By**: {createdByMention:string}" + }, + user_removed: { + title: "## User Removed", + action: "{actorMention:string} removed {targetMention:string} from {ticketChannelMention:string}.", + details: "**Ticket**: #{ticketId:string} - {ticketTypeName:string}\n**Opened By**: {createdByMention:string}" + } + } + } +}; + +export default en; diff --git a/i18n/formatters.ts b/i18n/formatters.ts new file mode 100644 index 00000000..822df6c5 --- /dev/null +++ b/i18n/formatters.ts @@ -0,0 +1,6 @@ +import type { FormattersInitializer } from "typesafe-i18n"; +import type { Formatters, Locales } from "./i18n-types.js"; + +export const initFormatters: FormattersInitializer = () => { + return {}; +}; diff --git a/i18n/fr/index.ts b/i18n/fr/index.ts new file mode 100644 index 00000000..9642c31d --- /dev/null +++ b/i18n/fr/index.ts @@ -0,0 +1,294 @@ +import type { Translation } from "../i18n-types.js"; + +const fr: Translation = { + shared: { + unexpected_interaction_error: "Une erreur inattendue s'est produite pendant le traitement de cette interaction.", + no_reason_provided: "Aucun détail supplémentaire n'a été fourni.", + claim_status: { + claimed_by: "Pris en charge par <@{userId}>", + unclaimed: "Non pris en charge" + }, + transcript_status: { + ready: "[Ouvrir la transcription]({url})", + unavailable: "Indisponible ou encore en cours de traitement." + } + }, + commands: { + add: { + description: "Ajouter quelqu'un au ticket actuel", + options: { + user: { + description: "L'utilisateur à ajouter" + } + }, + choose_user: "Choisissez un utilisateur à ajouter à ce ticket.", + already_has_access: "Cet utilisateur a déjà accès à ce ticket.", + already_invited: "Cet utilisateur est déjà invité dans ce ticket.", + invite_limit_reached: "Vous ne pouvez pas inviter plus de {limit} utilisateurs dans un ticket.", + success: "<@{userId}> a été ajouté à ce ticket." + }, + claim: { + description: "Prendre en charge le ticket actuel", + disabled: "La prise en charge des tickets est désactivée.", + already_claimed: "Vous avez déjà pris en charge ce ticket.", + cannot_take_over: "Ce ticket est déjà pris en charge et ne peut pas être repris.", + only_staff: "Seul le staff peut prendre en charge ce ticket.", + success: "Vous avez pris en charge ce ticket.", + reassigned: "Le ticket a été réattribué à <@{userId}>." + }, + close: { + description: "Fermer le ticket actuel" + }, + cleardm: { + description: "Effacer l'historique des tickets du bot dans vos MP", + starting: "Suppression de votre historique de tickets en MP...", + dm_unavailable: "Je n'ai pas pu accéder à votre salon MP.", + cleared: "{count} messages de ticket ont été supprimés de vos MP.", + none_found: "Aucun message de ticket n'a été trouvé dans vos MP." + }, + mass_add: { + description: "Ajouter plusieurs utilisateurs au ticket actuel", + options: { + users: { + description: "IDs d'utilisateurs ou mentions séparés par des virgules" + } + }, + provide_users: "Fournissez au moins un ID utilisateur ou une mention.", + summary: { + added: "Ajoutés: {mentions}.", + none_added: "Aucun utilisateur n'a été ajouté.", + skipped_existing: "{count} utilisateur(s) déjà autorisé(s) ont été ignorés.", + skipped_invalid: "{count} ID(s) utilisateur invalides ont été ignorés.", + limit_reached: "Arrêt lorsque la limite de {limit} utilisateurs par ticket a été atteinte." + } + }, + remove: { + description: "Retirer des utilisateurs invités du ticket actuel", + options: { + user: { + description: "L'utilisateur invité à retirer immédiatement" + } + }, + no_invited_users: "Il n'y a aucun utilisateur invité à retirer de ce ticket.", + select_users: "Sélectionnez les utilisateurs invités à retirer de ce ticket.", + select_placeholder: "Choisir des utilisateurs à retirer", + not_invited: "Ces utilisateurs ne sont pas invités dans ce ticket.", + success: "{mentions} ont été retirés de ce ticket." + }, + rename: { + description: "Renommer le ticket actuel", + options: { + name: { + description: "Le nouveau nom du salon du ticket" + } + }, + only_staff: "Seul le staff peut renommer ce ticket.", + provide_name: "Fournissez un nouveau nom de ticket.", + success: "Le ticket a été renommé en <#{channelId}>." + }, + unclaim: { + description: "Libérer le ticket actuel", + disabled: "La libération des tickets est désactivée pour ce serveur.", + not_claimed: "Ce ticket n'est pas pris en charge.", + only_current_claimer: "Seul le membre qui a pris en charge ce ticket peut le libérer.", + success: "Vous avez libéré ce ticket." + } + }, + tickets: { + records: { + not_ticket_channel: "Cette interaction n'a pas été utilisée dans un salon de ticket.", + not_open_ticket: "Ce salon n'est pas un ticket ouvert.", + already_closed: "Ce ticket est déjà fermé." + }, + panel: { + no_visible_types: "Vous n'avez accès à aucun type de ticket sur ce panneau.", + select_type: "Veuillez sélectionner un type de ticket.", + unavailable_type: "Ce type de ticket n'est pas disponible depuis ce panneau.", + select_placeholder: "Sélectionner un type de ticket" + }, + open: { + not_allowed_type: "Vous n'êtes pas autorisé à créer ce type de ticket.", + unavailable_type: "Ce type de ticket n'est pas disponible depuis ce panneau.", + max_open_reached: "Vous avez déjà atteint le nombre maximum de tickets ouverts ({limit}).", + created: "Votre ticket a été créé : <#{channelId}>", + question_answer: "{label} : {answer}" + }, + claim: { + only_staff: "Seul le staff peut prendre en charge ce ticket." + }, + actions: { + close_ticket: "Fermer le ticket", + claim_ticket: "Prendre en charge", + unclaim_ticket: "Libérer le ticket", + delete_ticket: "Supprimer le ticket" + }, + close: { + delete_channel_start: "Suppression du salon du ticket...", + modal: { + title: "Fermer le ticket", + reason_label: "Raison", + reason_placeholder: "Pourquoi ce ticket est-il fermé ?" + }, + status: { + preparing_transcript: "Préparation de la transcription...", + closing_ticket: "Fermeture du ticket...", + updating_access: "Mise à jour des accès au ticket...", + transcript_still_processing: "La transcription est encore en cours. Fin de la fermeture du ticket...", + sending_close_confirmation: "Envoi de la confirmation de fermeture...", + sending_close_updates: "Envoi des mises à jour de fermeture...", + posting_close_summary: "Publication du résumé de fermeture...", + closed: "Ticket fermé." + }, + deleted_with_transcript: "Ticket fermé. La transcription est prête et le salon va maintenant être supprimé.", + deleted_without_transcript: "Ticket fermé. Le salon va maintenant être supprimé.", + only_staff: "Seul le staff peut fermer ce ticket.", + must_be_claimed: "Ce ticket doit être pris en charge avant de pouvoir être fermé.", + only_current_claimer: "Seul le membre qui a pris en charge ce ticket peut le fermer.", + not_ticket: "Ce salon n'est pas un ticket.", + only_closed_delete: "Seuls les tickets fermés peuvent être supprimés depuis ce bouton.", + only_staff_delete: "Seul le staff peut supprimer ce ticket." + }, + transcript: { + collecting_messages: "Collecte des messages du ticket...", + creating: "Création de la transcription...", + uploading: "Envoi de la transcription...", + uploading_avatars: "Envoi des avatars...", + uploading_attachments: "Envoi des pièces jointes...", + progress: "{label} ({completed}/{total})" + }, + templates: { + open_panel: { + title: "## Ouvrir un ticket", + description: "Choisissez la catégorie correspondant à votre demande et le bot créera un ticket privé pour vous." + }, + ticket_opened: { + title: "## Ticket {ticketTypeName}", + intro: "Merci d'avoir ouvert un ticket.", + details_label: "**Détails**\n{reason}", + claim_status: "**Statut de prise en charge** : {claimStatus}" + }, + ticket_opened_general: { + title: "## Ticket d'assistance générale", + intro: "Un membre du support examinera votre demande sous peu.", + details_label: "**Résumé**\n{reason}", + claim_status: "**Statut de prise en charge** : {claimStatus}" + }, + ticket_opened_billing: { + title: "## Ticket de facturation", + intro: "Incluez les numéros de facture, le moyen de paiement et les détails des transactions en échec.", + details_label: "**Détails envoyés**\n{reason}", + claim_status: "**Statut de prise en charge** : {claimStatus}" + }, + ticket_opened_report: { + title: "## Ticket de signalement", + intro: "L'équipe de modération examinera le signalement et les preuves jointes.", + details_label: "**Détails du signalement**\n{reason}", + claim_status: "**Statut de prise en charge** : {claimStatus}" + }, + ticket_closed: { + title: "## Ticket fermé", + subtitle: "Le ticket de <@{userId}> a été fermé.", + details: "**Raison** : {reason}\n**Prise en charge** : {claimStatus}\n**Transcription** : {transcriptStatus}", + closed_by: "-# _Fermé par {closerName}_" + }, + ticket_closed_general: { + title: "## Assistance générale fermée", + subtitle: "Le ticket d'assistance générale de <@{userId}> est maintenant fermé.", + details: "**Raison** : {reason}\n**Prise en charge** : {claimStatus}\n**Transcription** : {transcriptStatus}", + closed_by: "-# _Fermé par {closerName}_" + }, + ticket_closed_billing: { + title: "## Ticket de facturation fermé", + subtitle: "Le ticket de facturation de <@{userId}> a été fermé.", + details: + "**Raison de fermeture** : {reason}\n**Prise en charge** : {claimStatus}\n**Transcription** : {transcriptStatus}", + closed_by: "-# _Fermé par {closerName}_" + }, + ticket_closed_report: { + title: "## Dossier de signalement fermé", + subtitle: "Le signalement ouvert par <@{userId}> a été fermé.", + details: "**Note de résolution** : {reason}\n**Prise en charge** : {claimStatus}\n**Transcription** : {transcriptStatus}", + closed_by: "-# _Fermé par {closerName}_" + }, + ticket_closed_dm: { + title: "## Votre ticket a été fermé", + details: "**Raison** : {reason}\n**Prise en charge** : {claimStatus}\n**Transcription** : {transcriptStatus}", + closed_by: "-# _Fermé par {closerName}_" + }, + ticket_closed_dm_general: { + title: "## Votre ticket d'assistance générale a été fermé", + details: "**Raison** : {reason}\n**Prise en charge** : {claimStatus}\n**Transcription** : {transcriptStatus}", + closed_by: "-# _Fermé par {closerName}_" + }, + ticket_closed_dm_billing: { + title: "## Votre ticket de facturation a été fermé", + intro: + "Si vous avez encore besoin d'aide, ouvrez un nouveau ticket de facturation et ajoutez de nouveau les détails de votre commande.", + details: "**Raison** : {reason}\n**Prise en charge** : {claimStatus}\n**Transcription** : {transcriptStatus}", + closed_by: "-# _Fermé par {closerName}_" + }, + ticket_closed_dm_report: { + title: "## Votre ticket de signalement a été fermé", + intro: "Le staff a examiné le signalement et les preuves jointes.", + details: "**Note de résolution** : {reason}\n**Prise en charge** : {claimStatus}\n**Transcription** : {transcriptStatus}", + closed_by: "-# _Fermé par {closerName}_" + } + } + }, + logs: { + duration: { + day_short: "j", + hour_short: "h", + minute_short: "m", + second_short: "s" + }, + templates: { + ticket_created: { + title: "## Ticket créé", + action: "{actorMention} a ouvert {ticketChannelMention}.", + details: + "**Ticket** : #{ticketId} - {ticketTypeName}\n**Ouvert par** : {createdByMention}\n**Créé** : {createdAt}\n**Raison** : {reason}" + }, + ticket_claimed: { + title: "## Ticket pris en charge", + action: "{actorMention} a pris en charge {ticketChannelMention}.", + details: "**Ticket** : #{ticketId} - {ticketTypeName}\n**Ouvert par** : {createdByMention}\n**Ancienneté** : {ticketAge}" + }, + ticket_unclaimed: { + title: "## Ticket libéré", + action: "{actorMention} a libéré {ticketChannelMention}.", + details: "**Ticket** : #{ticketId} - {ticketTypeName}\n**Ouvert par** : {createdByMention}\n**Ancienneté** : {ticketAge}" + }, + ticket_closed: { + title: "## Ticket fermé", + action: "{actorMention} a fermé {ticketChannelMention}.", + details: + "**Ticket** : #{ticketId} - {ticketTypeName}\n**Ouvert par** : {createdByMention}\n**Statut de prise en charge** : {claimStatus}\n**Ancienneté** : {ticketAge}\n**Raison** : {reason}\n**Transcription** : {transcriptStatus}" + }, + ticket_deleted: { + title: "## Ticket supprimé", + action: "{actorMention} a supprimé {ticketChannelMention}.", + details: + "**Ticket** : #{ticketId} - {ticketTypeName}\n**Ouvert par** : {createdByMention}\n**Statut de prise en charge** : {claimStatus}\n**Ancienneté** : {ticketAge}\n**Raison de fermeture** : {reason}\n**Transcription** : {transcriptStatus}" + }, + ticket_renamed: { + title: "## Ticket renommé", + action: "{actorMention} a renommé {ticketChannelMention}.", + details: + "**Ticket** : #{ticketId} - {ticketTypeName}\n**Ouvert par** : {createdByMention}\n**De** : `{oldChannelName}`\n**Vers** : `{newChannelName}`" + }, + user_added: { + title: "## Utilisateur ajouté", + action: "{actorMention} a ajouté {targetMention} à {ticketChannelMention}.", + details: "**Ticket** : #{ticketId} - {ticketTypeName}\n**Ouvert par** : {createdByMention}" + }, + user_removed: { + title: "## Utilisateur retiré", + action: "{actorMention} a retiré {targetMention} de {ticketChannelMention}.", + details: "**Ticket** : #{ticketId} - {ticketTypeName}\n**Ouvert par** : {createdByMention}" + } + } + } +}; + +export default fr; diff --git a/i18n/i18n-types.ts b/i18n/i18n-types.ts new file mode 100644 index 00000000..2ba81c8c --- /dev/null +++ b/i18n/i18n-types.ts @@ -0,0 +1,1850 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +// biome-ignore-all lint: autogenerated file +import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from "typesafe-i18n"; + +export type BaseTranslation = BaseTranslationType; +export type BaseLocale = "en"; + +export type Locales = "en" | "fr"; + +export type Translation = RootTranslation; + +export type Translations = RootTranslation; + +type RootTranslation = { + shared: { + /** + * A​n​ ​u​n​e​x​p​e​c​t​e​d​ ​e​r​r​o​r​ ​o​c​c​u​r​r​e​d​ ​w​h​i​l​e​ ​h​a​n​d​l​i​n​g​ ​t​h​i​s​ ​i​n​t​e​r​a​c​t​i​o​n​. + */ + unexpected_interaction_error: string; + /** + * N​o​ ​a​d​d​i​t​i​o​n​a​l​ ​d​e​t​a​i​l​s​ ​w​e​r​e​ ​p​r​o​v​i​d​e​d​. + */ + no_reason_provided: string; + claim_status: { + /** + * C​l​a​i​m​e​d​ ​b​y​ ​<​@​{​u​s​e​r​I​d​}​> + * @param {string} userId + */ + claimed_by: RequiredParams<"userId">; + /** + * U​n​c​l​a​i​m​e​d + */ + unclaimed: string; + }; + transcript_status: { + /** + * [​O​p​e​n​ ​T​r​a​n​s​c​r​i​p​t​]​(​{​u​r​l​}​) + * @param {string} url + */ + ready: RequiredParams<"url">; + /** + * U​n​a​v​a​i​l​a​b​l​e​ ​o​r​ ​s​t​i​l​l​ ​p​r​o​c​e​s​s​i​n​g​. + */ + unavailable: string; + }; + }; + commands: { + add: { + /** + * A​d​d​ ​s​o​m​e​o​n​e​ ​t​o​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​t​i​c​k​e​t + */ + description: string; + options: { + user: { + /** + * T​h​e​ ​u​s​e​r​ ​t​o​ ​a​d​d + */ + description: string; + }; + }; + /** + * C​h​o​o​s​e​ ​a​ ​u​s​e​r​ ​t​o​ ​a​d​d​ ​t​o​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + choose_user: string; + /** + * T​h​a​t​ ​u​s​e​r​ ​a​l​r​e​a​d​y​ ​h​a​s​ ​a​c​c​e​s​s​ ​t​o​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + already_has_access: string; + /** + * T​h​a​t​ ​u​s​e​r​ ​i​s​ ​a​l​r​e​a​d​y​ ​i​n​v​i​t​e​d​ ​t​o​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + already_invited: string; + /** + * Y​o​u​ ​c​a​n​n​o​t​ ​i​n​v​i​t​e​ ​m​o​r​e​ ​t​h​a​n​ ​{​l​i​m​i​t​}​ ​u​s​e​r​s​ ​t​o​ ​o​n​e​ ​t​i​c​k​e​t​. + * @param {number} limit + */ + invite_limit_reached: RequiredParams<"limit">; + /** + * A​d​d​e​d​ ​<​@​{​u​s​e​r​I​d​}​>​ ​t​o​ ​t​h​i​s​ ​t​i​c​k​e​t​. + * @param {string} userId + */ + success: RequiredParams<"userId">; + }; + claim: { + /** + * C​l​a​i​m​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​t​i​c​k​e​t + */ + description: string; + /** + * T​i​c​k​e​t​ ​c​l​a​i​m​i​n​g​ ​i​s​ ​d​i​s​a​b​l​e​d​. + */ + disabled: string; + /** + * Y​o​u​ ​a​l​r​e​a​d​y​ ​c​l​a​i​m​e​d​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + already_claimed: string; + /** + * T​h​i​s​ ​t​i​c​k​e​t​ ​i​s​ ​a​l​r​e​a​d​y​ ​c​l​a​i​m​e​d​ ​a​n​d​ ​c​a​n​n​o​t​ ​b​e​ ​t​a​k​e​n​ ​o​v​e​r​. + */ + cannot_take_over: string; + /** + * O​n​l​y​ ​s​t​a​f​f​ ​c​a​n​ ​c​l​a​i​m​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + only_staff: string; + /** + * Y​o​u​ ​c​l​a​i​m​e​d​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + success: string; + /** + * T​i​c​k​e​t​ ​r​e​a​s​s​i​g​n​e​d​ ​t​o​ ​<​@​{​u​s​e​r​I​d​}​>​. + * @param {string} userId + */ + reassigned: RequiredParams<"userId">; + }; + close: { + /** + * C​l​o​s​e​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​t​i​c​k​e​t + */ + description: string; + }; + cleardm: { + /** + * C​l​e​a​r​ ​t​h​e​ ​b​o​t​'​s​ ​t​i​c​k​e​t​ ​h​i​s​t​o​r​y​ ​f​r​o​m​ ​y​o​u​r​ ​D​M​s + */ + description: string; + /** + * C​l​e​a​r​i​n​g​ ​y​o​u​r​ ​t​i​c​k​e​t​ ​D​M​ ​h​i​s​t​o​r​y​.​.​. + */ + starting: string; + /** + * I​ ​c​o​u​l​d​ ​n​o​t​ ​a​c​c​e​s​s​ ​y​o​u​r​ ​D​M​ ​c​h​a​n​n​e​l​. + */ + dm_unavailable: string; + /** + * C​l​e​a​r​e​d​ ​{​c​o​u​n​t​}​ ​t​i​c​k​e​t​ ​D​M​ ​m​e​s​s​a​g​e​s​. + * @param {number} count + */ + cleared: RequiredParams<"count">; + /** + * N​o​ ​t​i​c​k​e​t​ ​D​M​ ​m​e​s​s​a​g​e​s​ ​w​e​r​e​ ​f​o​u​n​d​. + */ + none_found: string; + }; + mass_add: { + /** + * A​d​d​ ​m​u​l​t​i​p​l​e​ ​u​s​e​r​s​ ​t​o​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​t​i​c​k​e​t + */ + description: string; + options: { + users: { + /** + * C​o​m​m​a​-​s​e​p​a​r​a​t​e​d​ ​u​s​e​r​ ​I​D​s​ ​o​r​ ​m​e​n​t​i​o​n​s + */ + description: string; + }; + }; + /** + * P​r​o​v​i​d​e​ ​a​t​ ​l​e​a​s​t​ ​o​n​e​ ​u​s​e​r​ ​I​D​ ​o​r​ ​m​e​n​t​i​o​n​. + */ + provide_users: string; + summary: { + /** + * A​d​d​e​d​ ​{​m​e​n​t​i​o​n​s​}​. + * @param {string} mentions + */ + added: RequiredParams<"mentions">; + /** + * N​o​ ​u​s​e​r​s​ ​w​e​r​e​ ​a​d​d​e​d​. + */ + none_added: string; + /** + * S​k​i​p​p​e​d​ ​{​c​o​u​n​t​}​ ​u​s​e​r​(​s​)​ ​t​h​a​t​ ​a​l​r​e​a​d​y​ ​h​a​d​ ​a​c​c​e​s​s​. + * @param {number} count + */ + skipped_existing: RequiredParams<"count">; + /** + * S​k​i​p​p​e​d​ ​{​c​o​u​n​t​}​ ​i​n​v​a​l​i​d​ ​u​s​e​r​ ​I​D​(​s​)​. + * @param {number} count + */ + skipped_invalid: RequiredParams<"count">; + /** + * S​t​o​p​p​e​d​ ​w​h​e​n​ ​t​h​e​ ​{​l​i​m​i​t​}​-​u​s​e​r​ ​t​i​c​k​e​t​ ​l​i​m​i​t​ ​w​a​s​ ​r​e​a​c​h​e​d​. + * @param {number} limit + */ + limit_reached: RequiredParams<"limit">; + }; + }; + remove: { + /** + * R​e​m​o​v​e​ ​i​n​v​i​t​e​d​ ​u​s​e​r​s​ ​f​r​o​m​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​t​i​c​k​e​t + */ + description: string; + options: { + user: { + /** + * T​h​e​ ​i​n​v​i​t​e​d​ ​u​s​e​r​ ​t​o​ ​r​e​m​o​v​e​ ​i​m​m​e​d​i​a​t​e​l​y + */ + description: string; + }; + }; + /** + * T​h​e​r​e​ ​a​r​e​ ​n​o​ ​i​n​v​i​t​e​d​ ​u​s​e​r​s​ ​t​o​ ​r​e​m​o​v​e​ ​f​r​o​m​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + no_invited_users: string; + /** + * S​e​l​e​c​t​ ​t​h​e​ ​i​n​v​i​t​e​d​ ​u​s​e​r​s​ ​y​o​u​ ​w​a​n​t​ ​t​o​ ​r​e​m​o​v​e​ ​f​r​o​m​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + select_users: string; + /** + * C​h​o​o​s​e​ ​u​s​e​r​s​ ​t​o​ ​r​e​m​o​v​e + */ + select_placeholder: string; + /** + * T​h​o​s​e​ ​u​s​e​r​s​ ​a​r​e​ ​n​o​t​ ​i​n​v​i​t​e​d​ ​t​o​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + not_invited: string; + /** + * R​e​m​o​v​e​d​ ​{​m​e​n​t​i​o​n​s​}​ ​f​r​o​m​ ​t​h​i​s​ ​t​i​c​k​e​t​. + * @param {string} mentions + */ + success: RequiredParams<"mentions">; + }; + rename: { + /** + * R​e​n​a​m​e​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​t​i​c​k​e​t + */ + description: string; + options: { + name: { + /** + * T​h​e​ ​n​e​w​ ​t​i​c​k​e​t​ ​c​h​a​n​n​e​l​ ​n​a​m​e + */ + description: string; + }; + }; + /** + * O​n​l​y​ ​s​t​a​f​f​ ​c​a​n​ ​r​e​n​a​m​e​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + only_staff: string; + /** + * P​r​o​v​i​d​e​ ​a​ ​n​e​w​ ​t​i​c​k​e​t​ ​n​a​m​e​. + */ + provide_name: string; + /** + * T​i​c​k​e​t​ ​r​e​n​a​m​e​d​ ​t​o​ ​<​#​{​c​h​a​n​n​e​l​I​d​}​>​. + * @param {string} channelId + */ + success: RequiredParams<"channelId">; + }; + unclaim: { + /** + * U​n​c​l​a​i​m​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​t​i​c​k​e​t + */ + description: string; + /** + * U​n​c​l​a​i​m​i​n​g​ ​i​s​ ​d​i​s​a​b​l​e​d​ ​f​o​r​ ​t​h​i​s​ ​s​e​r​v​e​r​. + */ + disabled: string; + /** + * T​h​i​s​ ​t​i​c​k​e​t​ ​i​s​ ​n​o​t​ ​c​l​a​i​m​e​d​. + */ + not_claimed: string; + /** + * O​n​l​y​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​c​l​a​i​m​e​r​ ​c​a​n​ ​u​n​c​l​a​i​m​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + only_current_claimer: string; + /** + * Y​o​u​ ​u​n​c​l​a​i​m​e​d​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + success: string; + }; + }; + tickets: { + records: { + /** + * T​h​i​s​ ​i​n​t​e​r​a​c​t​i​o​n​ ​w​a​s​ ​n​o​t​ ​u​s​e​d​ ​i​n​ ​a​ ​t​i​c​k​e​t​ ​c​h​a​n​n​e​l​. + */ + not_ticket_channel: string; + /** + * T​h​i​s​ ​c​h​a​n​n​e​l​ ​i​s​ ​n​o​t​ ​a​n​ ​o​p​e​n​ ​t​i​c​k​e​t​. + */ + not_open_ticket: string; + /** + * T​h​i​s​ ​t​i​c​k​e​t​ ​i​s​ ​a​l​r​e​a​d​y​ ​c​l​o​s​e​d​. + */ + already_closed: string; + }; + panel: { + /** + * Y​o​u​ ​d​o​ ​n​o​t​ ​h​a​v​e​ ​a​c​c​e​s​s​ ​t​o​ ​a​n​y​ ​t​i​c​k​e​t​ ​t​y​p​e​s​ ​o​n​ ​t​h​i​s​ ​p​a​n​e​l​. + */ + no_visible_types: string; + /** + * P​l​e​a​s​e​ ​s​e​l​e​c​t​ ​a​ ​t​i​c​k​e​t​ ​t​y​p​e​. + */ + select_type: string; + /** + * T​h​a​t​ ​t​i​c​k​e​t​ ​t​y​p​e​ ​i​s​ ​n​o​t​ ​a​v​a​i​l​a​b​l​e​ ​f​r​o​m​ ​t​h​i​s​ ​p​a​n​e​l​. + */ + unavailable_type: string; + /** + * S​e​l​e​c​t​ ​a​ ​t​i​c​k​e​t​ ​t​y​p​e + */ + select_placeholder: string; + }; + open: { + /** + * Y​o​u​ ​a​r​e​ ​n​o​t​ ​a​l​l​o​w​e​d​ ​t​o​ ​c​r​e​a​t​e​ ​t​h​a​t​ ​t​i​c​k​e​t​ ​t​y​p​e​. + */ + not_allowed_type: string; + /** + * T​h​a​t​ ​t​i​c​k​e​t​ ​t​y​p​e​ ​i​s​ ​n​o​t​ ​a​v​a​i​l​a​b​l​e​ ​f​r​o​m​ ​t​h​i​s​ ​p​a​n​e​l​. + */ + unavailable_type: string; + /** + * Y​o​u​ ​a​l​r​e​a​d​y​ ​h​a​v​e​ ​t​h​e​ ​m​a​x​i​m​u​m​ ​n​u​m​b​e​r​ ​o​f​ ​o​p​e​n​ ​t​i​c​k​e​t​s​ ​(​{​l​i​m​i​t​}​)​. + * @param {number} limit + */ + max_open_reached: RequiredParams<"limit">; + /** + * Y​o​u​r​ ​t​i​c​k​e​t​ ​h​a​s​ ​b​e​e​n​ ​c​r​e​a​t​e​d​:​ ​<​#​{​c​h​a​n​n​e​l​I​d​}​> + * @param {string} channelId + */ + created: RequiredParams<"channelId">; + /** + * {​l​a​b​e​l​}​:​ ​{​a​n​s​w​e​r​} + * @param {string} answer + * @param {string} label + */ + question_answer: RequiredParams<"answer" | "label">; + }; + claim: { + /** + * O​n​l​y​ ​s​t​a​f​f​ ​c​a​n​ ​c​l​a​i​m​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + only_staff: string; + }; + actions: { + /** + * C​l​o​s​e​ ​T​i​c​k​e​t + */ + close_ticket: string; + /** + * C​l​a​i​m​ ​T​i​c​k​e​t + */ + claim_ticket: string; + /** + * U​n​c​l​a​i​m​ ​T​i​c​k​e​t + */ + unclaim_ticket: string; + /** + * D​e​l​e​t​e​ ​T​i​c​k​e​t + */ + delete_ticket: string; + }; + close: { + /** + * D​e​l​e​t​i​n​g​ ​t​i​c​k​e​t​ ​c​h​a​n​n​e​l​.​.​. + */ + delete_channel_start: string; + modal: { + /** + * C​l​o​s​e​ ​T​i​c​k​e​t + */ + title: string; + /** + * R​e​a​s​o​n + */ + reason_label: string; + /** + * W​h​y​ ​i​s​ ​t​h​i​s​ ​t​i​c​k​e​t​ ​b​e​i​n​g​ ​c​l​o​s​e​d​? + */ + reason_placeholder: string; + }; + status: { + /** + * P​r​e​p​a​r​i​n​g​ ​t​r​a​n​s​c​r​i​p​t​.​.​. + */ + preparing_transcript: string; + /** + * C​l​o​s​i​n​g​ ​t​i​c​k​e​t​.​.​. + */ + closing_ticket: string; + /** + * U​p​d​a​t​i​n​g​ ​t​i​c​k​e​t​ ​a​c​c​e​s​s​.​.​. + */ + updating_access: string; + /** + * T​r​a​n​s​c​r​i​p​t​ ​i​s​ ​s​t​i​l​l​ ​p​r​o​c​e​s​s​i​n​g​.​ ​F​i​n​i​s​h​i​n​g​ ​t​i​c​k​e​t​ ​c​l​o​s​e​.​.​. + */ + transcript_still_processing: string; + /** + * S​e​n​d​i​n​g​ ​c​l​o​s​e​ ​c​o​n​f​i​r​m​a​t​i​o​n​.​.​. + */ + sending_close_confirmation: string; + /** + * S​e​n​d​i​n​g​ ​c​l​o​s​e​ ​u​p​d​a​t​e​s​.​.​. + */ + sending_close_updates: string; + /** + * P​o​s​t​i​n​g​ ​c​l​o​s​e​ ​s​u​m​m​a​r​y​.​.​. + */ + posting_close_summary: string; + /** + * T​i​c​k​e​t​ ​c​l​o​s​e​d​. + */ + closed: string; + }; + /** + * T​i​c​k​e​t​ ​c​l​o​s​e​d​.​ ​T​h​e​ ​t​r​a​n​s​c​r​i​p​t​ ​i​s​ ​r​e​a​d​y​ ​a​n​d​ ​t​h​e​ ​c​h​a​n​n​e​l​ ​w​i​l​l​ ​n​o​w​ ​b​e​ ​d​e​l​e​t​e​d​. + */ + deleted_with_transcript: string; + /** + * T​i​c​k​e​t​ ​c​l​o​s​e​d​.​ ​T​h​e​ ​c​h​a​n​n​e​l​ ​w​i​l​l​ ​n​o​w​ ​b​e​ ​d​e​l​e​t​e​d​. + */ + deleted_without_transcript: string; + /** + * O​n​l​y​ ​s​t​a​f​f​ ​c​a​n​ ​c​l​o​s​e​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + only_staff: string; + /** + * T​h​i​s​ ​t​i​c​k​e​t​ ​m​u​s​t​ ​b​e​ ​c​l​a​i​m​e​d​ ​b​e​f​o​r​e​ ​i​t​ ​c​a​n​ ​b​e​ ​c​l​o​s​e​d​. + */ + must_be_claimed: string; + /** + * O​n​l​y​ ​t​h​e​ ​c​u​r​r​e​n​t​ ​c​l​a​i​m​e​r​ ​c​a​n​ ​c​l​o​s​e​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + only_current_claimer: string; + /** + * T​h​i​s​ ​c​h​a​n​n​e​l​ ​i​s​ ​n​o​t​ ​a​ ​t​i​c​k​e​t​. + */ + not_ticket: string; + /** + * O​n​l​y​ ​c​l​o​s​e​d​ ​t​i​c​k​e​t​s​ ​c​a​n​ ​b​e​ ​d​e​l​e​t​e​d​ ​f​r​o​m​ ​t​h​i​s​ ​b​u​t​t​o​n​. + */ + only_closed_delete: string; + /** + * O​n​l​y​ ​s​t​a​f​f​ ​c​a​n​ ​d​e​l​e​t​e​ ​t​h​i​s​ ​t​i​c​k​e​t​. + */ + only_staff_delete: string; + }; + transcript: { + /** + * C​o​l​l​e​c​t​i​n​g​ ​t​i​c​k​e​t​ ​m​e​s​s​a​g​e​s​.​.​. + */ + collecting_messages: string; + /** + * C​r​e​a​t​i​n​g​ ​t​r​a​n​s​c​r​i​p​t​.​.​. + */ + creating: string; + /** + * U​p​l​o​a​d​i​n​g​ ​t​r​a​n​s​c​r​i​p​t​.​.​. + */ + uploading: string; + /** + * U​p​l​o​a​d​i​n​g​ ​a​v​a​t​a​r​s​.​.​. + */ + uploading_avatars: string; + /** + * U​p​l​o​a​d​i​n​g​ ​a​t​t​a​c​h​m​e​n​t​s​.​.​. + */ + uploading_attachments: string; + /** + * {​l​a​b​e​l​}​ ​(​{​c​o​m​p​l​e​t​e​d​}​/​{​t​o​t​a​l​}​) + * @param {number} completed + * @param {string} label + * @param {number} total + */ + progress: RequiredParams<"completed" | "label" | "total">; + }; + templates: { + open_panel: { + /** + * #​#​ ​O​p​e​n​ ​a​ ​T​i​c​k​e​t + */ + title: string; + /** + * C​h​o​o​s​e​ ​t​h​e​ ​c​a​t​e​g​o​r​y​ ​t​h​a​t​ ​m​a​t​c​h​e​s​ ​y​o​u​r​ ​r​e​q​u​e​s​t​ ​a​n​d​ ​t​h​e​ ​b​o​t​ ​w​i​l​l​ ​c​r​e​a​t​e​ ​a​ ​p​r​i​v​a​t​e​ ​t​i​c​k​e​t​ ​f​o​r​ ​y​o​u​. + */ + description: string; + }; + ticket_opened: { + /** + * #​#​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ ​T​i​c​k​e​t + * @param {string} ticketTypeName + */ + title: RequiredParams<"ticketTypeName">; + /** + * T​h​a​n​k​s​ ​f​o​r​ ​o​p​e​n​i​n​g​ ​a​ ​t​i​c​k​e​t​. + */ + intro: string; + /** + * *​*​D​e​t​a​i​l​s​*​*​ + ​{​r​e​a​s​o​n​} + * @param {string} reason + */ + details_label: RequiredParams<"reason">; + /** + * *​*​C​l​a​i​m​ ​S​t​a​t​u​s​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​} + * @param {string} claimStatus + */ + claim_status: RequiredParams<"claimStatus">; + }; + ticket_opened_general: { + /** + * #​#​ ​G​e​n​e​r​a​l​ ​S​u​p​p​o​r​t​ ​T​i​c​k​e​t + */ + title: string; + /** + * A​ ​s​u​p​p​o​r​t​ ​t​e​a​m​ ​m​e​m​b​e​r​ ​w​i​l​l​ ​r​e​v​i​e​w​ ​t​h​i​s​ ​r​e​q​u​e​s​t​ ​s​o​o​n​. + */ + intro: string; + /** + * *​*​S​u​m​m​a​r​y​*​*​ + ​{​r​e​a​s​o​n​} + * @param {string} reason + */ + details_label: RequiredParams<"reason">; + /** + * *​*​C​l​a​i​m​ ​S​t​a​t​u​s​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​} + * @param {string} claimStatus + */ + claim_status: RequiredParams<"claimStatus">; + }; + ticket_opened_billing: { + /** + * #​#​ ​B​i​l​l​i​n​g​ ​T​i​c​k​e​t + */ + title: string; + /** + * I​n​c​l​u​d​e​ ​i​n​v​o​i​c​e​ ​n​u​m​b​e​r​s​,​ ​p​a​y​m​e​n​t​ ​m​e​t​h​o​d​,​ ​a​n​d​ ​a​n​y​ ​f​a​i​l​e​d​ ​t​r​a​n​s​a​c​t​i​o​n​ ​d​e​t​a​i​l​s​. + */ + intro: string; + /** + * *​*​S​u​b​m​i​t​t​e​d​ ​D​e​t​a​i​l​s​*​*​ + ​{​r​e​a​s​o​n​} + * @param {string} reason + */ + details_label: RequiredParams<"reason">; + /** + * *​*​C​l​a​i​m​ ​S​t​a​t​u​s​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​} + * @param {string} claimStatus + */ + claim_status: RequiredParams<"claimStatus">; + }; + ticket_opened_report: { + /** + * #​#​ ​R​e​p​o​r​t​ ​T​i​c​k​e​t + */ + title: string; + /** + * M​o​d​e​r​a​t​i​o​n​ ​s​t​a​f​f​ ​w​i​l​l​ ​r​e​v​i​e​w​ ​t​h​e​ ​r​e​p​o​r​t​ ​a​n​d​ ​a​n​y​ ​e​v​i​d​e​n​c​e​ ​a​t​t​a​c​h​e​d​. + */ + intro: string; + /** + * *​*​R​e​p​o​r​t​ ​D​e​t​a​i​l​s​*​*​ + ​{​r​e​a​s​o​n​} + * @param {string} reason + */ + details_label: RequiredParams<"reason">; + /** + * *​*​C​l​a​i​m​ ​S​t​a​t​u​s​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​} + * @param {string} claimStatus + */ + claim_status: RequiredParams<"claimStatus">; + }; + ticket_closed: { + /** + * #​#​ ​T​i​c​k​e​t​ ​C​l​o​s​e​d + */ + title: string; + /** + * <​@​{​u​s​e​r​I​d​}​>​'​s​ ​t​i​c​k​e​t​ ​h​a​s​ ​b​e​e​n​ ​c​l​o​s​e​d​. + * @param {string} userId + */ + subtitle: RequiredParams<"userId">; + /** + * *​*​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​C​l​a​i​m​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} reason + * @param {string} transcriptStatus + */ + details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">; + /** + * -​#​ ​_​C​l​o​s​e​d​ ​b​y​ ​{​c​l​o​s​e​r​N​a​m​e​}​_ + * @param {string} closerName + */ + closed_by: RequiredParams<"closerName">; + }; + ticket_closed_general: { + /** + * #​#​ ​G​e​n​e​r​a​l​ ​S​u​p​p​o​r​t​ ​C​l​o​s​e​d + */ + title: string; + /** + * <​@​{​u​s​e​r​I​d​}​>​'​s​ ​g​e​n​e​r​a​l​ ​s​u​p​p​o​r​t​ ​t​i​c​k​e​t​ ​i​s​ ​n​o​w​ ​c​l​o​s​e​d​. + * @param {string} userId + */ + subtitle: RequiredParams<"userId">; + /** + * *​*​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​C​l​a​i​m​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} reason + * @param {string} transcriptStatus + */ + details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">; + /** + * -​#​ ​_​C​l​o​s​e​d​ ​b​y​ ​{​c​l​o​s​e​r​N​a​m​e​}​_ + * @param {string} closerName + */ + closed_by: RequiredParams<"closerName">; + }; + ticket_closed_billing: { + /** + * #​#​ ​B​i​l​l​i​n​g​ ​T​i​c​k​e​t​ ​C​l​o​s​e​d + */ + title: string; + /** + * <​@​{​u​s​e​r​I​d​}​>​'​s​ ​b​i​l​l​i​n​g​ ​t​i​c​k​e​t​ ​h​a​s​ ​b​e​e​n​ ​c​l​o​s​e​d​. + * @param {string} userId + */ + subtitle: RequiredParams<"userId">; + /** + * *​*​C​l​o​s​e​ ​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​C​l​a​i​m​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} reason + * @param {string} transcriptStatus + */ + details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">; + /** + * -​#​ ​_​C​l​o​s​e​d​ ​b​y​ ​{​c​l​o​s​e​r​N​a​m​e​}​_ + * @param {string} closerName + */ + closed_by: RequiredParams<"closerName">; + }; + ticket_closed_report: { + /** + * #​#​ ​R​e​p​o​r​t​ ​C​a​s​e​ ​C​l​o​s​e​d + */ + title: string; + /** + * T​h​e​ ​r​e​p​o​r​t​ ​o​p​e​n​e​d​ ​b​y​ ​<​@​{​u​s​e​r​I​d​}​>​ ​h​a​s​ ​b​e​e​n​ ​c​l​o​s​e​d​. + * @param {string} userId + */ + subtitle: RequiredParams<"userId">; + /** + * *​*​R​e​s​o​l​u​t​i​o​n​ ​N​o​t​e​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​C​l​a​i​m​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} reason + * @param {string} transcriptStatus + */ + details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">; + /** + * -​#​ ​_​C​l​o​s​e​d​ ​b​y​ ​{​c​l​o​s​e​r​N​a​m​e​}​_ + * @param {string} closerName + */ + closed_by: RequiredParams<"closerName">; + }; + ticket_closed_dm: { + /** + * #​#​ ​Y​o​u​r​ ​t​i​c​k​e​t​ ​h​a​s​ ​b​e​e​n​ ​c​l​o​s​e​d + */ + title: string; + /** + * *​*​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​C​l​a​i​m​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} reason + * @param {string} transcriptStatus + */ + details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">; + /** + * -​#​ ​_​C​l​o​s​e​d​ ​b​y​ ​{​c​l​o​s​e​r​N​a​m​e​}​_ + * @param {string} closerName + */ + closed_by: RequiredParams<"closerName">; + }; + ticket_closed_dm_general: { + /** + * #​#​ ​Y​o​u​r​ ​g​e​n​e​r​a​l​ ​s​u​p​p​o​r​t​ ​t​i​c​k​e​t​ ​h​a​s​ ​b​e​e​n​ ​c​l​o​s​e​d + */ + title: string; + /** + * *​*​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​C​l​a​i​m​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} reason + * @param {string} transcriptStatus + */ + details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">; + /** + * -​#​ ​_​C​l​o​s​e​d​ ​b​y​ ​{​c​l​o​s​e​r​N​a​m​e​}​_ + * @param {string} closerName + */ + closed_by: RequiredParams<"closerName">; + }; + ticket_closed_dm_billing: { + /** + * #​#​ ​Y​o​u​r​ ​b​i​l​l​i​n​g​ ​t​i​c​k​e​t​ ​h​a​s​ ​b​e​e​n​ ​c​l​o​s​e​d + */ + title: string; + /** + * I​f​ ​y​o​u​ ​s​t​i​l​l​ ​n​e​e​d​ ​h​e​l​p​,​ ​o​p​e​n​ ​a​ ​n​e​w​ ​b​i​l​l​i​n​g​ ​t​i​c​k​e​t​ ​a​n​d​ ​i​n​c​l​u​d​e​ ​y​o​u​r​ ​o​r​d​e​r​ ​d​e​t​a​i​l​s​ ​a​g​a​i​n​. + */ + intro: string; + /** + * *​*​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​C​l​a​i​m​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} reason + * @param {string} transcriptStatus + */ + details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">; + /** + * -​#​ ​_​C​l​o​s​e​d​ ​b​y​ ​{​c​l​o​s​e​r​N​a​m​e​}​_ + * @param {string} closerName + */ + closed_by: RequiredParams<"closerName">; + }; + ticket_closed_dm_report: { + /** + * #​#​ ​Y​o​u​r​ ​r​e​p​o​r​t​ ​t​i​c​k​e​t​ ​h​a​s​ ​b​e​e​n​ ​c​l​o​s​e​d + */ + title: string; + /** + * S​t​a​f​f​ ​r​e​v​i​e​w​e​d​ ​t​h​e​ ​r​e​p​o​r​t​ ​a​n​d​ ​a​n​y​ ​a​t​t​a​c​h​e​d​ ​e​v​i​d​e​n​c​e​. + */ + intro: string; + /** + * *​*​R​e​s​o​l​u​t​i​o​n​ ​N​o​t​e​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​C​l​a​i​m​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} reason + * @param {string} transcriptStatus + */ + details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">; + /** + * -​#​ ​_​C​l​o​s​e​d​ ​b​y​ ​{​c​l​o​s​e​r​N​a​m​e​}​_ + * @param {string} closerName + */ + closed_by: RequiredParams<"closerName">; + }; + }; + }; + logs: { + duration: { + /** + * d + */ + day_short: string; + /** + * h + */ + hour_short: string; + /** + * m + */ + minute_short: string; + /** + * s + */ + second_short: string; + }; + templates: { + ticket_created: { + /** + * #​#​ ​T​i​c​k​e​t​ ​C​r​e​a​t​e​d + */ + title: string; + /** + * {​a​c​t​o​r​M​e​n​t​i​o​n​}​ ​o​p​e​n​e​d​ ​{​t​i​c​k​e​t​C​h​a​n​n​e​l​M​e​n​t​i​o​n​}​. + * @param {string} actorMention + * @param {string} ticketChannelMention + */ + action: RequiredParams<"actorMention" | "ticketChannelMention">; + /** + * *​*​T​i​c​k​e​t​*​*​:​ ​#​{​t​i​c​k​e​t​I​d​}​ ​-​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ + ​*​*​O​p​e​n​e​d​ ​B​y​*​*​:​ ​{​c​r​e​a​t​e​d​B​y​M​e​n​t​i​o​n​}​ + ​*​*​C​r​e​a​t​e​d​*​*​:​ ​{​c​r​e​a​t​e​d​A​t​}​ + ​*​*​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​} + * @param {string} createdAt + * @param {string} createdByMention + * @param {string} reason + * @param {string} ticketId + * @param {string} ticketTypeName + */ + details: RequiredParams<"createdAt" | "createdByMention" | "reason" | "ticketId" | "ticketTypeName">; + }; + ticket_claimed: { + /** + * #​#​ ​T​i​c​k​e​t​ ​C​l​a​i​m​e​d + */ + title: string; + /** + * {​a​c​t​o​r​M​e​n​t​i​o​n​}​ ​c​l​a​i​m​e​d​ ​{​t​i​c​k​e​t​C​h​a​n​n​e​l​M​e​n​t​i​o​n​}​. + * @param {string} actorMention + * @param {string} ticketChannelMention + */ + action: RequiredParams<"actorMention" | "ticketChannelMention">; + /** + * *​*​T​i​c​k​e​t​*​*​:​ ​#​{​t​i​c​k​e​t​I​d​}​ ​-​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ + ​*​*​O​p​e​n​e​d​ ​B​y​*​*​:​ ​{​c​r​e​a​t​e​d​B​y​M​e​n​t​i​o​n​}​ + ​*​*​O​p​e​n​ ​A​g​e​*​*​:​ ​{​t​i​c​k​e​t​A​g​e​} + * @param {string} createdByMention + * @param {string} ticketAge + * @param {string} ticketId + * @param {string} ticketTypeName + */ + details: RequiredParams<"createdByMention" | "ticketAge" | "ticketId" | "ticketTypeName">; + }; + ticket_unclaimed: { + /** + * #​#​ ​T​i​c​k​e​t​ ​U​n​c​l​a​i​m​e​d + */ + title: string; + /** + * {​a​c​t​o​r​M​e​n​t​i​o​n​}​ ​u​n​c​l​a​i​m​e​d​ ​{​t​i​c​k​e​t​C​h​a​n​n​e​l​M​e​n​t​i​o​n​}​. + * @param {string} actorMention + * @param {string} ticketChannelMention + */ + action: RequiredParams<"actorMention" | "ticketChannelMention">; + /** + * *​*​T​i​c​k​e​t​*​*​:​ ​#​{​t​i​c​k​e​t​I​d​}​ ​-​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ + ​*​*​O​p​e​n​e​d​ ​B​y​*​*​:​ ​{​c​r​e​a​t​e​d​B​y​M​e​n​t​i​o​n​}​ + ​*​*​O​p​e​n​ ​A​g​e​*​*​:​ ​{​t​i​c​k​e​t​A​g​e​} + * @param {string} createdByMention + * @param {string} ticketAge + * @param {string} ticketId + * @param {string} ticketTypeName + */ + details: RequiredParams<"createdByMention" | "ticketAge" | "ticketId" | "ticketTypeName">; + }; + ticket_closed: { + /** + * #​#​ ​T​i​c​k​e​t​ ​C​l​o​s​e​d + */ + title: string; + /** + * {​a​c​t​o​r​M​e​n​t​i​o​n​}​ ​c​l​o​s​e​d​ ​{​t​i​c​k​e​t​C​h​a​n​n​e​l​M​e​n​t​i​o​n​}​. + * @param {string} actorMention + * @param {string} ticketChannelMention + */ + action: RequiredParams<"actorMention" | "ticketChannelMention">; + /** + * *​*​T​i​c​k​e​t​*​*​:​ ​#​{​t​i​c​k​e​t​I​d​}​ ​-​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ + ​*​*​O​p​e​n​e​d​ ​B​y​*​*​:​ ​{​c​r​e​a​t​e​d​B​y​M​e​n​t​i​o​n​}​ + ​*​*​C​l​a​i​m​ ​S​t​a​t​u​s​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​O​p​e​n​ ​A​g​e​*​*​:​ ​{​t​i​c​k​e​t​A​g​e​}​ + ​*​*​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} createdByMention + * @param {string} reason + * @param {string} ticketAge + * @param {string} ticketId + * @param {string} ticketTypeName + * @param {string} transcriptStatus + */ + details: RequiredParams< + "claimStatus" | "createdByMention" | "reason" | "ticketAge" | "ticketId" | "ticketTypeName" | "transcriptStatus" + >; + }; + ticket_deleted: { + /** + * #​#​ ​T​i​c​k​e​t​ ​D​e​l​e​t​e​d + */ + title: string; + /** + * {​a​c​t​o​r​M​e​n​t​i​o​n​}​ ​d​e​l​e​t​e​d​ ​{​t​i​c​k​e​t​C​h​a​n​n​e​l​M​e​n​t​i​o​n​}​. + * @param {string} actorMention + * @param {string} ticketChannelMention + */ + action: RequiredParams<"actorMention" | "ticketChannelMention">; + /** + * *​*​T​i​c​k​e​t​*​*​:​ ​#​{​t​i​c​k​e​t​I​d​}​ ​-​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ + ​*​*​O​p​e​n​e​d​ ​B​y​*​*​:​ ​{​c​r​e​a​t​e​d​B​y​M​e​n​t​i​o​n​}​ + ​*​*​C​l​a​i​m​ ​S​t​a​t​u​s​*​*​:​ ​{​c​l​a​i​m​S​t​a​t​u​s​}​ + ​*​*​O​p​e​n​ ​A​g​e​*​*​:​ ​{​t​i​c​k​e​t​A​g​e​}​ + ​*​*​C​l​o​s​e​ ​R​e​a​s​o​n​*​*​:​ ​{​r​e​a​s​o​n​}​ + ​*​*​T​r​a​n​s​c​r​i​p​t​*​*​:​ ​{​t​r​a​n​s​c​r​i​p​t​S​t​a​t​u​s​} + * @param {string} claimStatus + * @param {string} createdByMention + * @param {string} reason + * @param {string} ticketAge + * @param {string} ticketId + * @param {string} ticketTypeName + * @param {string} transcriptStatus + */ + details: RequiredParams< + "claimStatus" | "createdByMention" | "reason" | "ticketAge" | "ticketId" | "ticketTypeName" | "transcriptStatus" + >; + }; + ticket_renamed: { + /** + * #​#​ ​T​i​c​k​e​t​ ​R​e​n​a​m​e​d + */ + title: string; + /** + * {​a​c​t​o​r​M​e​n​t​i​o​n​}​ ​r​e​n​a​m​e​d​ ​{​t​i​c​k​e​t​C​h​a​n​n​e​l​M​e​n​t​i​o​n​}​. + * @param {string} actorMention + * @param {string} ticketChannelMention + */ + action: RequiredParams<"actorMention" | "ticketChannelMention">; + /** + * *​*​T​i​c​k​e​t​*​*​:​ ​#​{​t​i​c​k​e​t​I​d​}​ ​-​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ + ​*​*​O​p​e​n​e​d​ ​B​y​*​*​:​ ​{​c​r​e​a​t​e​d​B​y​M​e​n​t​i​o​n​}​ + ​*​*​F​r​o​m​*​*​:​ ​`​{​o​l​d​C​h​a​n​n​e​l​N​a​m​e​}​`​ + ​*​*​T​o​*​*​:​ ​`​{​n​e​w​C​h​a​n​n​e​l​N​a​m​e​}​` + * @param {string} createdByMention + * @param {string} newChannelName + * @param {string} oldChannelName + * @param {string} ticketId + * @param {string} ticketTypeName + */ + details: RequiredParams<"createdByMention" | "newChannelName" | "oldChannelName" | "ticketId" | "ticketTypeName">; + }; + user_added: { + /** + * #​#​ ​U​s​e​r​ ​A​d​d​e​d + */ + title: string; + /** + * {​a​c​t​o​r​M​e​n​t​i​o​n​}​ ​a​d​d​e​d​ ​{​t​a​r​g​e​t​M​e​n​t​i​o​n​}​ ​t​o​ ​{​t​i​c​k​e​t​C​h​a​n​n​e​l​M​e​n​t​i​o​n​}​. + * @param {string} actorMention + * @param {string} targetMention + * @param {string} ticketChannelMention + */ + action: RequiredParams<"actorMention" | "targetMention" | "ticketChannelMention">; + /** + * *​*​T​i​c​k​e​t​*​*​:​ ​#​{​t​i​c​k​e​t​I​d​}​ ​-​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ + ​*​*​O​p​e​n​e​d​ ​B​y​*​*​:​ ​{​c​r​e​a​t​e​d​B​y​M​e​n​t​i​o​n​} + * @param {string} createdByMention + * @param {string} ticketId + * @param {string} ticketTypeName + */ + details: RequiredParams<"createdByMention" | "ticketId" | "ticketTypeName">; + }; + user_removed: { + /** + * #​#​ ​U​s​e​r​ ​R​e​m​o​v​e​d + */ + title: string; + /** + * {​a​c​t​o​r​M​e​n​t​i​o​n​}​ ​r​e​m​o​v​e​d​ ​{​t​a​r​g​e​t​M​e​n​t​i​o​n​}​ ​f​r​o​m​ ​{​t​i​c​k​e​t​C​h​a​n​n​e​l​M​e​n​t​i​o​n​}​. + * @param {string} actorMention + * @param {string} targetMention + * @param {string} ticketChannelMention + */ + action: RequiredParams<"actorMention" | "targetMention" | "ticketChannelMention">; + /** + * *​*​T​i​c​k​e​t​*​*​:​ ​#​{​t​i​c​k​e​t​I​d​}​ ​-​ ​{​t​i​c​k​e​t​T​y​p​e​N​a​m​e​}​ + ​*​*​O​p​e​n​e​d​ ​B​y​*​*​:​ ​{​c​r​e​a​t​e​d​B​y​M​e​n​t​i​o​n​} + * @param {string} createdByMention + * @param {string} ticketId + * @param {string} ticketTypeName + */ + details: RequiredParams<"createdByMention" | "ticketId" | "ticketTypeName">; + }; + }; + }; +}; + +export type TranslationFunctions = { + shared: { + /** + * An unexpected error occurred while handling this interaction. + */ + unexpected_interaction_error: () => LocalizedString; + /** + * No additional details were provided. + */ + no_reason_provided: () => LocalizedString; + claim_status: { + /** + * Claimed by <@{userId}> + */ + claimed_by: (arg: { userId: string }) => LocalizedString; + /** + * Unclaimed + */ + unclaimed: () => LocalizedString; + }; + transcript_status: { + /** + * [Open Transcript]({url}) + */ + ready: (arg: { url: string }) => LocalizedString; + /** + * Unavailable or still processing. + */ + unavailable: () => LocalizedString; + }; + }; + commands: { + add: { + /** + * Add someone to the current ticket + */ + description: () => LocalizedString; + options: { + user: { + /** + * The user to add + */ + description: () => LocalizedString; + }; + }; + /** + * Choose a user to add to this ticket. + */ + choose_user: () => LocalizedString; + /** + * That user already has access to this ticket. + */ + already_has_access: () => LocalizedString; + /** + * That user is already invited to this ticket. + */ + already_invited: () => LocalizedString; + /** + * You cannot invite more than {limit} users to one ticket. + */ + invite_limit_reached: (arg: { limit: number }) => LocalizedString; + /** + * Added <@{userId}> to this ticket. + */ + success: (arg: { userId: string }) => LocalizedString; + }; + claim: { + /** + * Claim the current ticket + */ + description: () => LocalizedString; + /** + * Ticket claiming is disabled. + */ + disabled: () => LocalizedString; + /** + * You already claimed this ticket. + */ + already_claimed: () => LocalizedString; + /** + * This ticket is already claimed and cannot be taken over. + */ + cannot_take_over: () => LocalizedString; + /** + * Only staff can claim this ticket. + */ + only_staff: () => LocalizedString; + /** + * You claimed this ticket. + */ + success: () => LocalizedString; + /** + * Ticket reassigned to <@{userId}>. + */ + reassigned: (arg: { userId: string }) => LocalizedString; + }; + close: { + /** + * Close the current ticket + */ + description: () => LocalizedString; + }; + cleardm: { + /** + * Clear the bot's ticket history from your DMs + */ + description: () => LocalizedString; + /** + * Clearing your ticket DM history... + */ + starting: () => LocalizedString; + /** + * I could not access your DM channel. + */ + dm_unavailable: () => LocalizedString; + /** + * Cleared {count} ticket DM messages. + */ + cleared: (arg: { count: number }) => LocalizedString; + /** + * No ticket DM messages were found. + */ + none_found: () => LocalizedString; + }; + mass_add: { + /** + * Add multiple users to the current ticket + */ + description: () => LocalizedString; + options: { + users: { + /** + * Comma-separated user IDs or mentions + */ + description: () => LocalizedString; + }; + }; + /** + * Provide at least one user ID or mention. + */ + provide_users: () => LocalizedString; + summary: { + /** + * Added {mentions}. + */ + added: (arg: { mentions: string }) => LocalizedString; + /** + * No users were added. + */ + none_added: () => LocalizedString; + /** + * Skipped {count} user(s) that already had access. + */ + skipped_existing: (arg: { count: number }) => LocalizedString; + /** + * Skipped {count} invalid user ID(s). + */ + skipped_invalid: (arg: { count: number }) => LocalizedString; + /** + * Stopped when the {limit}-user ticket limit was reached. + */ + limit_reached: (arg: { limit: number }) => LocalizedString; + }; + }; + remove: { + /** + * Remove invited users from the current ticket + */ + description: () => LocalizedString; + options: { + user: { + /** + * The invited user to remove immediately + */ + description: () => LocalizedString; + }; + }; + /** + * There are no invited users to remove from this ticket. + */ + no_invited_users: () => LocalizedString; + /** + * Select the invited users you want to remove from this ticket. + */ + select_users: () => LocalizedString; + /** + * Choose users to remove + */ + select_placeholder: () => LocalizedString; + /** + * Those users are not invited to this ticket. + */ + not_invited: () => LocalizedString; + /** + * Removed {mentions} from this ticket. + */ + success: (arg: { mentions: string }) => LocalizedString; + }; + rename: { + /** + * Rename the current ticket + */ + description: () => LocalizedString; + options: { + name: { + /** + * The new ticket channel name + */ + description: () => LocalizedString; + }; + }; + /** + * Only staff can rename this ticket. + */ + only_staff: () => LocalizedString; + /** + * Provide a new ticket name. + */ + provide_name: () => LocalizedString; + /** + * Ticket renamed to <#{channelId}>. + */ + success: (arg: { channelId: string }) => LocalizedString; + }; + unclaim: { + /** + * Unclaim the current ticket + */ + description: () => LocalizedString; + /** + * Unclaiming is disabled for this server. + */ + disabled: () => LocalizedString; + /** + * This ticket is not claimed. + */ + not_claimed: () => LocalizedString; + /** + * Only the current claimer can unclaim this ticket. + */ + only_current_claimer: () => LocalizedString; + /** + * You unclaimed this ticket. + */ + success: () => LocalizedString; + }; + }; + tickets: { + records: { + /** + * This interaction was not used in a ticket channel. + */ + not_ticket_channel: () => LocalizedString; + /** + * This channel is not an open ticket. + */ + not_open_ticket: () => LocalizedString; + /** + * This ticket is already closed. + */ + already_closed: () => LocalizedString; + }; + panel: { + /** + * You do not have access to any ticket types on this panel. + */ + no_visible_types: () => LocalizedString; + /** + * Please select a ticket type. + */ + select_type: () => LocalizedString; + /** + * That ticket type is not available from this panel. + */ + unavailable_type: () => LocalizedString; + /** + * Select a ticket type + */ + select_placeholder: () => LocalizedString; + }; + open: { + /** + * You are not allowed to create that ticket type. + */ + not_allowed_type: () => LocalizedString; + /** + * That ticket type is not available from this panel. + */ + unavailable_type: () => LocalizedString; + /** + * You already have the maximum number of open tickets ({limit}). + */ + max_open_reached: (arg: { limit: number }) => LocalizedString; + /** + * Your ticket has been created: <#{channelId}> + */ + created: (arg: { channelId: string }) => LocalizedString; + /** + * {label}: {answer} + */ + question_answer: (arg: { answer: string; label: string }) => LocalizedString; + }; + claim: { + /** + * Only staff can claim this ticket. + */ + only_staff: () => LocalizedString; + }; + actions: { + /** + * Close Ticket + */ + close_ticket: () => LocalizedString; + /** + * Claim Ticket + */ + claim_ticket: () => LocalizedString; + /** + * Unclaim Ticket + */ + unclaim_ticket: () => LocalizedString; + /** + * Delete Ticket + */ + delete_ticket: () => LocalizedString; + }; + close: { + /** + * Deleting ticket channel... + */ + delete_channel_start: () => LocalizedString; + modal: { + /** + * Close Ticket + */ + title: () => LocalizedString; + /** + * Reason + */ + reason_label: () => LocalizedString; + /** + * Why is this ticket being closed? + */ + reason_placeholder: () => LocalizedString; + }; + status: { + /** + * Preparing transcript... + */ + preparing_transcript: () => LocalizedString; + /** + * Closing ticket... + */ + closing_ticket: () => LocalizedString; + /** + * Updating ticket access... + */ + updating_access: () => LocalizedString; + /** + * Transcript is still processing. Finishing ticket close... + */ + transcript_still_processing: () => LocalizedString; + /** + * Sending close confirmation... + */ + sending_close_confirmation: () => LocalizedString; + /** + * Sending close updates... + */ + sending_close_updates: () => LocalizedString; + /** + * Posting close summary... + */ + posting_close_summary: () => LocalizedString; + /** + * Ticket closed. + */ + closed: () => LocalizedString; + }; + /** + * Ticket closed. The transcript is ready and the channel will now be deleted. + */ + deleted_with_transcript: () => LocalizedString; + /** + * Ticket closed. The channel will now be deleted. + */ + deleted_without_transcript: () => LocalizedString; + /** + * Only staff can close this ticket. + */ + only_staff: () => LocalizedString; + /** + * This ticket must be claimed before it can be closed. + */ + must_be_claimed: () => LocalizedString; + /** + * Only the current claimer can close this ticket. + */ + only_current_claimer: () => LocalizedString; + /** + * This channel is not a ticket. + */ + not_ticket: () => LocalizedString; + /** + * Only closed tickets can be deleted from this button. + */ + only_closed_delete: () => LocalizedString; + /** + * Only staff can delete this ticket. + */ + only_staff_delete: () => LocalizedString; + }; + transcript: { + /** + * Collecting ticket messages... + */ + collecting_messages: () => LocalizedString; + /** + * Creating transcript... + */ + creating: () => LocalizedString; + /** + * Uploading transcript... + */ + uploading: () => LocalizedString; + /** + * Uploading avatars... + */ + uploading_avatars: () => LocalizedString; + /** + * Uploading attachments... + */ + uploading_attachments: () => LocalizedString; + /** + * {label} ({completed}/{total}) + */ + progress: (arg: { completed: number; label: string; total: number }) => LocalizedString; + }; + templates: { + open_panel: { + /** + * ## Open a Ticket + */ + title: () => LocalizedString; + /** + * Choose the category that matches your request and the bot will create a private ticket for you. + */ + description: () => LocalizedString; + }; + ticket_opened: { + /** + * ## {ticketTypeName} Ticket + */ + title: (arg: { ticketTypeName: string }) => LocalizedString; + /** + * Thanks for opening a ticket. + */ + intro: () => LocalizedString; + /** + * **Details** + {reason} + */ + details_label: (arg: { reason: string }) => LocalizedString; + /** + * **Claim Status**: {claimStatus} + */ + claim_status: (arg: { claimStatus: string }) => LocalizedString; + }; + ticket_opened_general: { + /** + * ## General Support Ticket + */ + title: () => LocalizedString; + /** + * A support team member will review this request soon. + */ + intro: () => LocalizedString; + /** + * **Summary** + {reason} + */ + details_label: (arg: { reason: string }) => LocalizedString; + /** + * **Claim Status**: {claimStatus} + */ + claim_status: (arg: { claimStatus: string }) => LocalizedString; + }; + ticket_opened_billing: { + /** + * ## Billing Ticket + */ + title: () => LocalizedString; + /** + * Include invoice numbers, payment method, and any failed transaction details. + */ + intro: () => LocalizedString; + /** + * **Submitted Details** + {reason} + */ + details_label: (arg: { reason: string }) => LocalizedString; + /** + * **Claim Status**: {claimStatus} + */ + claim_status: (arg: { claimStatus: string }) => LocalizedString; + }; + ticket_opened_report: { + /** + * ## Report Ticket + */ + title: () => LocalizedString; + /** + * Moderation staff will review the report and any evidence attached. + */ + intro: () => LocalizedString; + /** + * **Report Details** + {reason} + */ + details_label: (arg: { reason: string }) => LocalizedString; + /** + * **Claim Status**: {claimStatus} + */ + claim_status: (arg: { claimStatus: string }) => LocalizedString; + }; + ticket_closed: { + /** + * ## Ticket Closed + */ + title: () => LocalizedString; + /** + * <@{userId}>'s ticket has been closed. + */ + subtitle: (arg: { userId: string }) => LocalizedString; + /** + * **Reason**: {reason} + **Claim**: {claimStatus} + **Transcript**: {transcriptStatus} + */ + details: (arg: { claimStatus: string; reason: string; transcriptStatus: string }) => LocalizedString; + /** + * -# _Closed by {closerName}_ + */ + closed_by: (arg: { closerName: string }) => LocalizedString; + }; + ticket_closed_general: { + /** + * ## General Support Closed + */ + title: () => LocalizedString; + /** + * <@{userId}>'s general support ticket is now closed. + */ + subtitle: (arg: { userId: string }) => LocalizedString; + /** + * **Reason**: {reason} + **Claim**: {claimStatus} + **Transcript**: {transcriptStatus} + */ + details: (arg: { claimStatus: string; reason: string; transcriptStatus: string }) => LocalizedString; + /** + * -# _Closed by {closerName}_ + */ + closed_by: (arg: { closerName: string }) => LocalizedString; + }; + ticket_closed_billing: { + /** + * ## Billing Ticket Closed + */ + title: () => LocalizedString; + /** + * <@{userId}>'s billing ticket has been closed. + */ + subtitle: (arg: { userId: string }) => LocalizedString; + /** + * **Close Reason**: {reason} + **Claim**: {claimStatus} + **Transcript**: {transcriptStatus} + */ + details: (arg: { claimStatus: string; reason: string; transcriptStatus: string }) => LocalizedString; + /** + * -# _Closed by {closerName}_ + */ + closed_by: (arg: { closerName: string }) => LocalizedString; + }; + ticket_closed_report: { + /** + * ## Report Case Closed + */ + title: () => LocalizedString; + /** + * The report opened by <@{userId}> has been closed. + */ + subtitle: (arg: { userId: string }) => LocalizedString; + /** + * **Resolution Note**: {reason} + **Claim**: {claimStatus} + **Transcript**: {transcriptStatus} + */ + details: (arg: { claimStatus: string; reason: string; transcriptStatus: string }) => LocalizedString; + /** + * -# _Closed by {closerName}_ + */ + closed_by: (arg: { closerName: string }) => LocalizedString; + }; + ticket_closed_dm: { + /** + * ## Your ticket has been closed + */ + title: () => LocalizedString; + /** + * **Reason**: {reason} + **Claim**: {claimStatus} + **Transcript**: {transcriptStatus} + */ + details: (arg: { claimStatus: string; reason: string; transcriptStatus: string }) => LocalizedString; + /** + * -# _Closed by {closerName}_ + */ + closed_by: (arg: { closerName: string }) => LocalizedString; + }; + ticket_closed_dm_general: { + /** + * ## Your general support ticket has been closed + */ + title: () => LocalizedString; + /** + * **Reason**: {reason} + **Claim**: {claimStatus} + **Transcript**: {transcriptStatus} + */ + details: (arg: { claimStatus: string; reason: string; transcriptStatus: string }) => LocalizedString; + /** + * -# _Closed by {closerName}_ + */ + closed_by: (arg: { closerName: string }) => LocalizedString; + }; + ticket_closed_dm_billing: { + /** + * ## Your billing ticket has been closed + */ + title: () => LocalizedString; + /** + * If you still need help, open a new billing ticket and include your order details again. + */ + intro: () => LocalizedString; + /** + * **Reason**: {reason} + **Claim**: {claimStatus} + **Transcript**: {transcriptStatus} + */ + details: (arg: { claimStatus: string; reason: string; transcriptStatus: string }) => LocalizedString; + /** + * -# _Closed by {closerName}_ + */ + closed_by: (arg: { closerName: string }) => LocalizedString; + }; + ticket_closed_dm_report: { + /** + * ## Your report ticket has been closed + */ + title: () => LocalizedString; + /** + * Staff reviewed the report and any attached evidence. + */ + intro: () => LocalizedString; + /** + * **Resolution Note**: {reason} + **Claim**: {claimStatus} + **Transcript**: {transcriptStatus} + */ + details: (arg: { claimStatus: string; reason: string; transcriptStatus: string }) => LocalizedString; + /** + * -# _Closed by {closerName}_ + */ + closed_by: (arg: { closerName: string }) => LocalizedString; + }; + }; + }; + logs: { + duration: { + /** + * d + */ + day_short: () => LocalizedString; + /** + * h + */ + hour_short: () => LocalizedString; + /** + * m + */ + minute_short: () => LocalizedString; + /** + * s + */ + second_short: () => LocalizedString; + }; + templates: { + ticket_created: { + /** + * ## Ticket Created + */ + title: () => LocalizedString; + /** + * {actorMention} opened {ticketChannelMention}. + */ + action: (arg: { actorMention: string; ticketChannelMention: string }) => LocalizedString; + /** + * **Ticket**: #{ticketId} - {ticketTypeName} + **Opened By**: {createdByMention} + **Created**: {createdAt} + **Reason**: {reason} + */ + details: (arg: { + createdAt: string; + createdByMention: string; + reason: string; + ticketId: string; + ticketTypeName: string; + }) => LocalizedString; + }; + ticket_claimed: { + /** + * ## Ticket Claimed + */ + title: () => LocalizedString; + /** + * {actorMention} claimed {ticketChannelMention}. + */ + action: (arg: { actorMention: string; ticketChannelMention: string }) => LocalizedString; + /** + * **Ticket**: #{ticketId} - {ticketTypeName} + **Opened By**: {createdByMention} + **Open Age**: {ticketAge} + */ + details: (arg: { + createdByMention: string; + ticketAge: string; + ticketId: string; + ticketTypeName: string; + }) => LocalizedString; + }; + ticket_unclaimed: { + /** + * ## Ticket Unclaimed + */ + title: () => LocalizedString; + /** + * {actorMention} unclaimed {ticketChannelMention}. + */ + action: (arg: { actorMention: string; ticketChannelMention: string }) => LocalizedString; + /** + * **Ticket**: #{ticketId} - {ticketTypeName} + **Opened By**: {createdByMention} + **Open Age**: {ticketAge} + */ + details: (arg: { + createdByMention: string; + ticketAge: string; + ticketId: string; + ticketTypeName: string; + }) => LocalizedString; + }; + ticket_closed: { + /** + * ## Ticket Closed + */ + title: () => LocalizedString; + /** + * {actorMention} closed {ticketChannelMention}. + */ + action: (arg: { actorMention: string; ticketChannelMention: string }) => LocalizedString; + /** + * **Ticket**: #{ticketId} - {ticketTypeName} + **Opened By**: {createdByMention} + **Claim Status**: {claimStatus} + **Open Age**: {ticketAge} + **Reason**: {reason} + **Transcript**: {transcriptStatus} + */ + details: (arg: { + claimStatus: string; + createdByMention: string; + reason: string; + ticketAge: string; + ticketId: string; + ticketTypeName: string; + transcriptStatus: string; + }) => LocalizedString; + }; + ticket_deleted: { + /** + * ## Ticket Deleted + */ + title: () => LocalizedString; + /** + * {actorMention} deleted {ticketChannelMention}. + */ + action: (arg: { actorMention: string; ticketChannelMention: string }) => LocalizedString; + /** + * **Ticket**: #{ticketId} - {ticketTypeName} + **Opened By**: {createdByMention} + **Claim Status**: {claimStatus} + **Open Age**: {ticketAge} + **Close Reason**: {reason} + **Transcript**: {transcriptStatus} + */ + details: (arg: { + claimStatus: string; + createdByMention: string; + reason: string; + ticketAge: string; + ticketId: string; + ticketTypeName: string; + transcriptStatus: string; + }) => LocalizedString; + }; + ticket_renamed: { + /** + * ## Ticket Renamed + */ + title: () => LocalizedString; + /** + * {actorMention} renamed {ticketChannelMention}. + */ + action: (arg: { actorMention: string; ticketChannelMention: string }) => LocalizedString; + /** + * **Ticket**: #{ticketId} - {ticketTypeName} + **Opened By**: {createdByMention} + **From**: `{oldChannelName}` + **To**: `{newChannelName}` + */ + details: (arg: { + createdByMention: string; + newChannelName: string; + oldChannelName: string; + ticketId: string; + ticketTypeName: string; + }) => LocalizedString; + }; + user_added: { + /** + * ## User Added + */ + title: () => LocalizedString; + /** + * {actorMention} added {targetMention} to {ticketChannelMention}. + */ + action: (arg: { actorMention: string; targetMention: string; ticketChannelMention: string }) => LocalizedString; + /** + * **Ticket**: #{ticketId} - {ticketTypeName} + **Opened By**: {createdByMention} + */ + details: (arg: { createdByMention: string; ticketId: string; ticketTypeName: string }) => LocalizedString; + }; + user_removed: { + /** + * ## User Removed + */ + title: () => LocalizedString; + /** + * {actorMention} removed {targetMention} from {ticketChannelMention}. + */ + action: (arg: { actorMention: string; targetMention: string; ticketChannelMention: string }) => LocalizedString; + /** + * **Ticket**: #{ticketId} - {ticketTypeName} + **Opened By**: {createdByMention} + */ + details: (arg: { createdByMention: string; ticketId: string; ticketTypeName: string }) => LocalizedString; + }; + }; + }; +}; + +export type Formatters = {}; diff --git a/i18n/i18n-util.async.ts b/i18n/i18n-util.async.ts new file mode 100644 index 00000000..c3e362b4 --- /dev/null +++ b/i18n/i18n-util.async.ts @@ -0,0 +1,26 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +// biome-ignore-all lint: autogenerated file + +import { initFormatters } from "./formatters.js"; +import type { Locales, Translations } from "./i18n-types.js"; +import { loadedFormatters, loadedLocales, locales } from "./i18n-util.js"; + +const localeTranslationLoaders = { + en: () => import("./en/index.js"), + fr: () => import("./fr/index.js") +}; + +const updateDictionary = (locale: Locales, dictionary: Partial): Translations => + (loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }); + +export const importLocaleAsync = async (locale: Locales): Promise => + (await localeTranslationLoaders[locale]()).default as unknown as Translations; + +export const loadLocaleAsync = async (locale: Locales): Promise => { + updateDictionary(locale, await importLocaleAsync(locale)); + loadFormatters(locale); +}; + +export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync)); + +export const loadFormatters = (locale: Locales): void => void (loadedFormatters[locale] = initFormatters(locale)); diff --git a/i18n/i18n-util.sync.ts b/i18n/i18n-util.sync.ts new file mode 100644 index 00000000..0313b21a --- /dev/null +++ b/i18n/i18n-util.sync.ts @@ -0,0 +1,25 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +// biome-ignore-all lint: autogenerated file + +import { initFormatters } from "./formatters.js"; +import type { Locales, Translations } from "./i18n-types.js"; +import { loadedFormatters, loadedLocales, locales } from "./i18n-util.js"; + +import en from "./en/index.js"; +import fr from "./fr/index.js"; + +const localeTranslations = { + en, + fr +}; + +export const loadLocale = (locale: Locales): void => { + if (loadedLocales[locale]) return; + + loadedLocales[locale] = localeTranslations[locale] as unknown as Translations; + loadFormatters(locale); +}; + +export const loadAllLocales = (): void => locales.forEach(loadLocale); + +export const loadFormatters = (locale: Locales): void => void (loadedFormatters[locale] = initFormatters(locale)); diff --git a/i18n/i18n-util.ts b/i18n/i18n-util.ts new file mode 100644 index 00000000..3b340b24 --- /dev/null +++ b/i18n/i18n-util.ts @@ -0,0 +1,37 @@ +// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. +// biome-ignore-all lint: autogenerated file + +import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from "typesafe-i18n"; +import type { LocaleDetector } from "typesafe-i18n/detectors"; +import type { LocaleTranslationFunctions, TranslateByString } from "typesafe-i18n"; +import { detectLocale as detectLocaleFn } from "typesafe-i18n/detectors"; +import { initExtendDictionary } from "typesafe-i18n/utils"; +import type { Formatters, Locales, Translations, TranslationFunctions } from "./i18n-types.js"; + +export const baseLocale: Locales = "en"; + +export const locales: Locales[] = ["en", "fr"]; + +export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales); + +export const loadedLocales: Record = {} as Record; + +export const loadedFormatters: Record = {} as Record; + +export const extendDictionary = initExtendDictionary(); + +export const i18nString = (locale: Locales): TranslateByString => + initI18nString(locale, loadedFormatters[locale]); + +export const i18nObject = (locale: Locales): TranslationFunctions => + initI18nObject( + locale, + loadedLocales[locale], + loadedFormatters[locale] + ); + +export const i18n = (): LocaleTranslationFunctions => + initI18n(loadedLocales, loadedFormatters); + +export const detectLocale = (...detectors: LocaleDetector[]): Locales => + detectLocaleFn(baseLocale, locales, ...detectors); diff --git a/src/app.ts b/src/app.ts index b00b3b05..35b9d59b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -18,6 +18,7 @@ import { REST } from "@discordjs/rest"; import { WebSocketManager } from "@discordjs/ws"; import { drizzle } from "drizzle-orm/libsql"; import { discoverCommands, discoverEvents, discoverFeatures } from "@/core/discovery"; +import { createBotI18n } from "@/core/i18n"; import { createLogger } from "@/core/logger"; import { createHandlerRegistry, registerEvents } from "@/core/registry"; import { InteractionRouter } from "@/core/router"; @@ -41,7 +42,8 @@ export async function createBotApp() { discoverEvents(logger), discoverFeatures(logger) ]); - const registry = createHandlerRegistry({ commands, features, events, logger }); + const i18n = createBotI18n(botConfig.lang, logger); + const registry = createHandlerRegistry({ commands, features, events, logger, LL: i18n.LL }); const app = {} as BotApp; app.client = client; @@ -49,6 +51,8 @@ export async function createBotApp() { app.config = botConfig; app.logger = logger; app.applicationId = botConfig.clientId; + app.locale = i18n.locale; + app.LL = i18n.LL; app.registry = registry; app.router = new InteractionRouter(app); diff --git a/src/config/index.ts b/src/config/index.ts index b9573a54..dccf63ec 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -13,13 +13,15 @@ project repository or to its website. This notice must not be removed, obscured, or replaced. */ +import type { Locales } from "../../i18n/i18n-types.js"; + interface ConfigV0_0_1 { /** The client ID of the bot application */ clientId: string; /** The ID of the Discord server the bot is set up in */ guildId: string; /** The lang of the bot */ - lang: "en"; + lang: Locales; /** Controls the transcript ID style when uploading to ticket.pm */ uuidType?: "uuid" | "emoji"; /** Reduce telemetry to bot/runtime version only */ diff --git a/src/core/discovery.ts b/src/core/discovery.ts index 5a60f4ea..36007ff3 100644 --- a/src/core/discovery.ts +++ b/src/core/discovery.ts @@ -28,10 +28,7 @@ function isCommandModule(value: unknown): value is CommandModule { typeof value === "object" && value !== null && "data" in value && - typeof value.data === "object" && - value.data !== null && - "name" in value.data && - typeof value.data.name === "string" && + (typeof value.data === "function" || (typeof value.data === "object" && value.data !== null)) && "execute" in value && typeof value.execute === "function" ); diff --git a/src/core/i18n.ts b/src/core/i18n.ts new file mode 100644 index 00000000..1a667cc5 --- /dev/null +++ b/src/core/i18n.ts @@ -0,0 +1,29 @@ +import type { Logger } from "@/core/logger"; +import type { Locales, TranslationFunctions } from "../../i18n/i18n-types.js"; +import { i18nObject, isLocale } from "../../i18n/i18n-util.js"; +import { loadLocale } from "../../i18n/i18n-util.sync.js"; + +export function createBotI18n( + requestedLocale: string, + logger?: Logger +): { + locale: Locales; + LL: TranslationFunctions; +} { + const locale = isLocale(requestedLocale) ? requestedLocale : "en"; + + try { + loadLocale(locale); + return { + locale, + LL: i18nObject(locale) + }; + } catch (error) { + logger?.warn(`Failed to load locale "${requestedLocale}". Falling back to "en".`, error); + loadLocale("en"); + return { + locale: "en", + LL: i18nObject("en") + }; + } +} diff --git a/src/core/registry.ts b/src/core/registry.ts index fcfc76ad..d6501d6c 100644 --- a/src/core/registry.ts +++ b/src/core/registry.ts @@ -15,17 +15,21 @@ This notice must not be removed, obscured, or replaced. import type { Logger } from "@/core/logger"; import type { BotApp, CommandModule, EventModule, FeatureModule, HandlerRegistry } from "@/core/types"; +import type { TranslationFunctions } from "../../i18n/i18n-types.js"; +import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord-api-types/v10"; interface CreateHandlerRegistryInput { commands: CommandModule[]; events: EventModule[]; features: FeatureModule[]; logger: Logger; + LL: TranslationFunctions; } -export function createHandlerRegistry({ commands, events, features, logger }: CreateHandlerRegistryInput): HandlerRegistry { +export function createHandlerRegistry({ commands, events, features, logger, LL }: CreateHandlerRegistryInput): HandlerRegistry { const featureMap = new Map(); const commandMap = new Map(); + const applicationCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = []; for (const feature of features) { if (featureMap.has(feature.key)) { @@ -36,11 +40,17 @@ export function createHandlerRegistry({ commands, events, features, logger }: Cr } for (const command of commands) { - if (commandMap.has(command.data.name)) { - throw new Error(`Duplicate slash command "${command.data.name}" detected.`); + const data = typeof command.data === "function" ? command.data(LL) : command.data; + + if (commandMap.has(data.name)) { + throw new Error(`Duplicate slash command "${data.name}" detected.`); } - commandMap.set(command.data.name, command); + commandMap.set(data.name, { + ...command, + data + }); + applicationCommands.push(data); } logger.info(`Registered ${featureMap.size} feature modules.`); @@ -49,7 +59,7 @@ export function createHandlerRegistry({ commands, events, features, logger }: Cr events, features: featureMap, commands: commandMap, - applicationCommands: [...commandMap.values()].map((command) => command.data) + applicationCommands }; } diff --git a/src/core/respond.ts b/src/core/respond.ts index 95f6e297..eba244c4 100644 --- a/src/core/respond.ts +++ b/src/core/respond.ts @@ -59,7 +59,7 @@ export async function replyWithAutocomplete(app: BotApp, interaction: APIApplica export async function replyWithError(app: BotApp, interaction: ReplyableInteraction) { return reply(app, interaction, { - content: "An unexpected error occurred while handling this interaction.", + content: app.LL.shared.unexpected_interaction_error(), flags: MessageFlags.Ephemeral }).catch(() => undefined); } diff --git a/src/core/types.ts b/src/core/types.ts index a6121b1e..abc270d8 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -23,6 +23,7 @@ import type { } from "@discordjs/core"; import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord-api-types/v10"; import type { drizzle } from "drizzle-orm/libsql"; +import type { Locales, TranslationFunctions } from "../../i18n/i18n-types.js"; import type { AnyVersionedConfig } from "@/config/index"; import type { ParsedCustomId } from "@/core/custom-id"; import type { Logger } from "@/core/logger"; @@ -33,8 +34,12 @@ export type RoutedInteraction = | APIMessageComponentInteraction | APIModalSubmitInteraction; +export type CommandDataResolver = + | RESTPostAPIChatInputApplicationCommandsJSONBody + | ((LL: TranslationFunctions) => RESTPostAPIChatInputApplicationCommandsJSONBody); + export interface CommandModule { - data: RESTPostAPIChatInputApplicationCommandsJSONBody; + data: CommandDataResolver; execute(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction): Promise; autocomplete?(context: CommandExecutionContext, interaction: APIApplicationCommandAutocompleteInteraction): Promise; } @@ -69,6 +74,8 @@ export interface BotApp { config: AnyVersionedConfig; applicationId: string; logger: Logger; + locale: Locales; + LL: TranslationFunctions; registry: HandlerRegistry; router: InteractionRouterContract; } From baf60c00de69d1bf4b39513a1b5147033c8c88f8 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:09:02 +0200 Subject: [PATCH 31/67] feat(tickets): integrate i18n in ticket workflows * Replace hardcoded strings with dictionary keys in workflows * Add text formatting utilities for shared strings * Update message template loading to pass locale context --- src/features/logs/service.ts | 30 +++++----- src/features/tickets/claim-workflow.ts | 20 +++---- src/features/tickets/close-workflow.ts | 78 ++++++++++++------------- src/features/tickets/messages.ts | 15 ++++- src/features/tickets/panel-sync.ts | 15 ++--- src/features/tickets/records.ts | 6 +- src/features/tickets/text.ts | 15 +++++ src/features/tickets/ticket-workflow.ts | 74 +++++++++++++---------- src/features/tickets/transcripts.ts | 20 ++++--- src/features/tickets/types.ts | 8 +++ 10 files changed, 164 insertions(+), 117 deletions(-) create mode 100644 src/features/tickets/text.ts diff --git a/src/features/logs/service.ts b/src/features/logs/service.ts index d499d0f3..d0a02ac7 100644 --- a/src/features/logs/service.ts +++ b/src/features/logs/service.ts @@ -15,8 +15,8 @@ This notice must not be removed, obscured, or replaced. import type { BotApp } from "@/core/types"; import type { TicketLogEvent } from "@/features/logs/types"; -import { DEFAULT_NO_REASON } from "@/features/tickets/constants"; import { finalizeMessageTemplate, loadMessageTemplate } from "@/features/tickets/messages"; +import { formatClaimStatus, formatTranscriptStatus, getDefaultNoReason } from "@/features/tickets/text"; import type { LogEventToggleKey } from "@/features/tickets/types"; const LOG_EVENT_TOGGLE_KEYS: Record = { @@ -52,7 +52,7 @@ export async function sendTicketLog(app: BotApp, event: TicketLogEvent) { const channelId = app.config.logs.channelId.trim(); try { - const messageTemplate = await loadMessageTemplate(LOG_TEMPLATE_REFERENCES[event.kind], createLogTokens(event)); + const messageTemplate = await loadMessageTemplate(app, LOG_TEMPLATE_REFERENCES[event.kind], createLogTokens(app, event)); const payload = finalizeMessageTemplate({ ...messageTemplate, allowed_mentions: messageTemplate.allowed_mentions ?? { @@ -79,29 +79,29 @@ export function shouldSendTicketLog(app: BotApp, kind: TicketLogEvent["kind"]) { return app.config.logs.events?.[toggleKey] ?? true; } -function createLogTokens(event: TicketLogEvent) { +function createLogTokens(app: BotApp, event: TicketLogEvent) { const openedAtSeconds = Math.floor(event.ticket.createdAt / 1000); const claimedById = resolveClaimedById(event); const tokens: Record = { actorId: event.actor.id, actorMention: `<@${event.actor.id}>`, actorName: event.actor.username, - claimStatus: claimedById ? `Claimed by <@${claimedById}>` : "Unclaimed", + claimStatus: formatClaimStatus(app, claimedById), claimerId: claimedById ?? undefined, claimerMention: claimedById ? `<@${claimedById}>` : undefined, createdAt: ``, createdById: event.ticket.createdById, createdByMention: `<@${event.ticket.createdById}>`, - reason: DEFAULT_NO_REASON, + reason: getDefaultNoReason(app), targetId: undefined, targetMention: undefined, - ticketAge: formatDuration(Date.now() - event.ticket.createdAt), + ticketAge: formatDuration(app, Date.now() - event.ticket.createdAt), ticketChannelId: event.ticket.ticketChannelId, ticketChannelMention: `<#${event.ticket.ticketChannelId}>`, ticketId: event.ticket.ticketId, ticketTypeKey: event.ticket.ticketTypeKey, ticketTypeName: event.ticket.ticketTypeName, - transcriptStatus: "Unavailable or still processing.", + transcriptStatus: formatTranscriptStatus(app, null), transcriptUrl: undefined }; @@ -113,9 +113,7 @@ function createLogTokens(event: TicketLogEvent) { case "ticketDelete": tokens.reason = event.reason; tokens.transcriptUrl = event.transcriptUrl ?? undefined; - tokens.transcriptStatus = event.transcriptUrl - ? `[Open Transcript](${event.transcriptUrl})` - : "Unavailable or still processing."; + tokens.transcriptStatus = formatTranscriptStatus(app, event.transcriptUrl); break; case "userAdded": case "userRemoved": @@ -141,18 +139,18 @@ function resolveClaimedById(event: TicketLogEvent) { } } -function formatDuration(durationMs: number) { +function formatDuration(app: BotApp, durationMs: number) { const totalSeconds = Math.max(0, Math.floor(durationMs / 1000)); if (totalSeconds < 60) { - return `${totalSeconds}s`; + return `${totalSeconds}${app.LL.logs.duration.second_short()}`; } const units: Array<[label: string, seconds: number]> = [ - ["d", 86_400], - ["h", 3_600], - ["m", 60], - ["s", 1] + [app.LL.logs.duration.day_short(), 86_400], + [app.LL.logs.duration.hour_short(), 3_600], + [app.LL.logs.duration.minute_short(), 60], + [app.LL.logs.duration.second_short(), 1] ]; const parts: string[] = []; let remainingSeconds = totalSeconds; diff --git a/src/features/tickets/claim-workflow.ts b/src/features/tickets/claim-workflow.ts index a0fe50d9..a61aec89 100644 --- a/src/features/tickets/claim-workflow.ts +++ b/src/features/tickets/claim-workflow.ts @@ -53,7 +53,7 @@ export async function handleUnclaimButton(context: ComponentExecutionContext, in async function claimTicket(app: BotApp, interaction: ClaimInteraction) { if (!app.config.tickets.claims.enabled) { - await replyWithContent(app, interaction, "Ticket claiming is disabled."); + await replyWithContent(app, interaction, app.LL.commands.claim.disabled()); return; } @@ -68,12 +68,12 @@ async function claimTicket(app: BotApp, interaction: ClaimInteraction) { const { ticket, ticketType } = claimable; if (ticket.claimedBy === actor.id) { - await replyWithContent(app, interaction, "You already claimed this ticket."); + await replyWithContent(app, interaction, app.LL.commands.claim.already_claimed()); return; } if (ticket.claimedBy && !canTakeOverClaim(app, getMemberRoleIds(interaction))) { - await replyWithContent(app, interaction, "This ticket is already claimed and cannot be taken over."); + await replyWithContent(app, interaction, app.LL.commands.claim.cannot_take_over()); return; } @@ -104,18 +104,18 @@ async function claimTicket(app: BotApp, interaction: ClaimInteraction) { await replyWithContent( app, interaction, - ticket.claimedBy ? `Ticket reassigned to <@${actor.id}>.` : `You claimed this ticket.` + ticket.claimedBy ? app.LL.commands.claim.reassigned({ userId: actor.id }) : app.LL.commands.claim.success() ); } async function unclaimTicket(app: BotApp, interaction: ClaimInteraction) { if (!app.config.tickets.claims.enabled) { - await replyWithContent(app, interaction, "Ticket claiming is disabled."); + await replyWithContent(app, interaction, app.LL.commands.claim.disabled()); return; } if (!app.config.tickets.claims.allowUnclaim) { - await replyWithContent(app, interaction, "Unclaiming is disabled for this server."); + await replyWithContent(app, interaction, app.LL.commands.unclaim.disabled()); return; } @@ -130,12 +130,12 @@ async function unclaimTicket(app: BotApp, interaction: ClaimInteraction) { const { ticket, ticketType } = claimable; if (!ticket.claimedBy) { - await replyWithContent(app, interaction, "This ticket is not claimed."); + await replyWithContent(app, interaction, app.LL.commands.unclaim.not_claimed()); return; } if (ticket.claimedBy !== actor.id) { - await replyWithContent(app, interaction, "Only the current claimer can unclaim this ticket."); + await replyWithContent(app, interaction, app.LL.commands.unclaim.only_current_claimer()); return; } @@ -168,7 +168,7 @@ async function unclaimTicket(app: BotApp, interaction: ClaimInteraction) { ) }); - await replyWithContent(app, interaction, "You unclaimed this ticket."); + await replyWithContent(app, interaction, app.LL.commands.unclaim.success()); } async function getClaimableTicket(app: BotApp, channelId: string | undefined, roleIds: string[]) { @@ -183,7 +183,7 @@ async function getClaimableTicket(app: BotApp, channelId: string | undefined, ro if (!hasTicketStaffAccess(app, ticketType, roleIds)) { return { ok: false as const, - message: "Only staff can claim this ticket." + message: app.LL.tickets.claim.only_staff() }; } diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index 1891fea1..4df274f3 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -31,7 +31,6 @@ import { ticketsTable } from "@/db/schema"; import { sendTicketLog } from "@/features/logs/service"; import { createTicketLogContext } from "@/features/logs/utils"; import { getTicketType, hasTicketStaffAccess } from "@/features/tickets/config-access"; -import { DEFAULT_NO_REASON } from "@/features/tickets/constants"; import { appendMessageButton, finalizeMessageTemplate, @@ -40,6 +39,7 @@ import { } from "@/features/tickets/messages"; import { getInvitedUserIds, revokeTicketParticipantAccess } from "@/features/tickets/participants"; import { findTicketByChannel, getOpenTicketByChannel } from "@/features/tickets/records"; +import { formatClaimStatus, formatTranscriptStatus, getDefaultNoReason } from "@/features/tickets/text"; import { startTranscriptJob } from "@/features/tickets/transcripts"; import { getInteractionUser, getMemberRoleIds } from "@/features/tickets/utils"; @@ -65,7 +65,7 @@ export async function handleDeleteClosedTicketButton( if (!channelId) { await reply(context.app, interaction, { - content: "This interaction was not used in a ticket channel.", + content: context.app.LL.tickets.records.not_ticket_channel(), flags: MessageFlags.Ephemeral }); return; @@ -82,13 +82,13 @@ export async function handleDeleteClosedTicketButton( } await reply(context.app, interaction, { - content: "Deleting ticket channel...", + content: context.app.LL.tickets.close.delete_channel_start(), flags: MessageFlags.Ephemeral }); void sendTicketLog(context.app, { kind: "ticketDelete", actor: getInteractionUser(interaction), - reason: manageable.ticket.closedReason ?? DEFAULT_NO_REASON, + reason: manageable.ticket.closedReason ?? getDefaultNoReason(context.app), transcriptUrl: manageable.ticket.transcriptUrl, ticket: createTicketLogContext(manageable.ticket, manageable.ticketType.name) }); @@ -123,7 +123,7 @@ async function beginCloseFlow( if (app.config.tickets.close.askForReason) { await showModal(app, interaction, { custom_id: createCustomId("tickets", "submit-close-reason"), - title: "Close Ticket", + title: app.LL.tickets.close.modal.title(), components: [ { type: ComponentType.ActionRow, @@ -131,11 +131,11 @@ async function beginCloseFlow( { type: ComponentType.TextInput, custom_id: "reason", - label: "Reason", + label: app.LL.tickets.close.modal.reason_label(), style: TextInputStyle.Paragraph, required: false, max_length: 500, - placeholder: "Why is this ticket being closed?" + placeholder: app.LL.tickets.close.modal.reason_placeholder() } ] } @@ -156,7 +156,7 @@ async function closeTicket( if (!channelId) { await reply(app, interaction, { - content: "This interaction was not used in a ticket channel.", + content: app.LL.tickets.records.not_ticket_channel(), flags: MessageFlags.Ephemeral }); return; @@ -174,11 +174,15 @@ async function closeTicket( } const status = createCloseStatusUpdater(app, interaction); - await status.start(app.config.tickets.close.createTranscript ? "Preparing transcript..." : "Closing ticket..."); + await status.start( + app.config.tickets.close.createTranscript + ? app.LL.tickets.close.status.preparing_transcript() + : app.LL.tickets.close.status.closing_ticket() + ); const { ticket, ticketType } = closable; const closer = getInteractionUser(interaction); - const normalizedReason = normalizeCloseReason(reason); + const normalizedReason = normalizeCloseReason(app, reason); // Mark the ticket as closed immediately so repeated button presses or `/close` // attempts during transcript generation do not start duplicate close flows. @@ -198,7 +202,7 @@ async function closeTicket( // Preserve the original ticket message while preventing new actions. await disableTicketActionButtons(app, ticket.channelId, ticket.creationMessageId); }); - await status.update("Updating ticket access..."); + await status.update(app.LL.tickets.close.status.updating_access()); await removeClosedTicketParticipantAccess(app, ticket.channelId, ticket.createdBy, invitedUserIds); } @@ -217,19 +221,19 @@ async function closeTicket( const transcriptUrl = transcriptJob ? await transcriptJob.waitForResult() : null; if (app.config.tickets.close.createTranscript && !transcriptUrl) { - await status.update("Transcript is still processing. Finishing ticket close..."); + await status.update(app.LL.tickets.close.status.transcript_still_processing()); } const closeMessageTokens = { channelId: ticket.channelId, - claimStatus: formatClaimStatus(ticket.claimedBy), + claimStatus: formatClaimStatus(app, ticket.claimedBy), claimerId: ticket.claimedBy ?? "", claimerMention: ticket.claimedBy ? `<@${ticket.claimedBy}>` : "", closerId: closer.id, closerMention: `<@${closer.id}>`, closerName: closer.username, reason: normalizedReason, - transcriptStatus: formatTranscriptStatus(transcriptUrl), + transcriptStatus: formatTranscriptStatus(app, transcriptUrl), transcriptUrl: transcriptUrl ?? "", userId: ticket.createdBy }; @@ -242,7 +246,7 @@ async function closeTicket( }); if (app.config.tickets.close.deleteChannelOnClose && app.config.tickets.close.dmUserOnClose) { - await status.update("Sending close confirmation..."); + await status.update(app.LL.tickets.close.status.sending_close_confirmation()); await sendCloseDm(app, ticket.createdBy, ticketType, closeMessageTokens); } @@ -255,9 +259,7 @@ async function closeTicket( ticket: createTicketLogContext(ticket, ticketType.name) }); await editReply(app, interaction, { - content: transcriptUrl - ? "Ticket closed. The transcript is ready and the channel will now be deleted." - : "Ticket closed. The channel will now be deleted." + content: transcriptUrl ? app.LL.tickets.close.deleted_with_transcript() : app.LL.tickets.close.deleted_without_transcript() }); await app.client.api.channels.delete(ticket.channelId); return; @@ -270,10 +272,14 @@ async function closeTicket( closeTasks.push(sendCloseDm(app, ticket.createdBy, ticketType, closeMessageTokens)); } - await status.update(app.config.tickets.close.dmUserOnClose ? "Sending close updates..." : "Posting close summary..."); + await status.update( + app.config.tickets.close.dmUserOnClose + ? app.LL.tickets.close.status.sending_close_updates() + : app.LL.tickets.close.status.posting_close_summary() + ); await Promise.all(closeTasks); - await status.update("Ticket closed."); + await status.update(app.LL.tickets.close.status.closed()); } async function getClosableTicket( @@ -294,7 +300,7 @@ async function getClosableTicket( if (enforcePermission && app.config.tickets.close.staffOnly && !hasTicketStaffAccess(app, ticketType, roleIds)) { return { ok: false as const, - message: "Only staff can close this ticket." + message: app.LL.tickets.close.only_staff() }; } @@ -302,14 +308,14 @@ async function getClosableTicket( if (!ticket.claimedBy) { return { ok: false as const, - message: "This ticket must be claimed before it can be closed." + message: app.LL.tickets.close.must_be_claimed() }; } if (ticket.claimedBy !== actorId) { return { ok: false as const, - message: "Only the current claimer can close this ticket." + message: app.LL.tickets.close.only_current_claimer() }; } } @@ -325,7 +331,7 @@ async function getDeletableTicket(app: BotApp, channelId: string | undefined, ro if (!channelId) { return { ok: false as const, - message: "This interaction was not used in a ticket channel." + message: app.LL.tickets.records.not_ticket_channel() }; } @@ -334,14 +340,14 @@ async function getDeletableTicket(app: BotApp, channelId: string | undefined, ro if (!ticket) { return { ok: false as const, - message: "This channel is not a ticket." + message: app.LL.tickets.close.not_ticket() }; } if (!ticket.closedAt) { return { ok: false as const, - message: "Only closed tickets can be deleted from this button." + message: app.LL.tickets.close.only_closed_delete() }; } @@ -350,7 +356,7 @@ async function getDeletableTicket(app: BotApp, channelId: string | undefined, ro if (!hasTicketStaffAccess(app, ticketType, roleIds)) { return { ok: false as const, - message: "Only staff can delete this ticket." + message: app.LL.tickets.close.only_staff_delete() }; } @@ -493,7 +499,7 @@ async function sendCloseDm( return; } - const messageTemplate = await loadMessageTemplate(resolveCloseDmMessageReference(app, ticketType), tokens); + const messageTemplate = await loadMessageTemplate(app, resolveCloseDmMessageReference(app, ticketType), tokens); await app.client.api.channels .createMessage(dmChannel.id, { @@ -520,7 +526,7 @@ async function buildCloseChannelMessage( } ) { const deleteButtonCustomId = createCustomId("tickets", "delete-closed"); - const messageTemplate = await loadMessageTemplate(resolveCloseChannelMessageReference(app, ticketType), { + const messageTemplate = await loadMessageTemplate(app, resolveCloseChannelMessageReference(app, ticketType), { ...tokens, deleteButtonCustomId }); @@ -532,7 +538,7 @@ async function buildCloseChannelMessage( ? ({ type: ComponentType.Button, custom_id: deleteButtonCustomId, - label: "Delete Ticket", + label: app.LL.tickets.actions.delete_ticket(), style: ButtonStyle.Danger } satisfies APIButtonComponentWithCustomId) : undefined @@ -599,14 +605,6 @@ function createCloseStatusUpdater( }; } -function formatTranscriptStatus(transcriptUrl: string | null) { - return transcriptUrl ? `[Open Transcript](${transcriptUrl})` : "Unavailable or still processing."; -} - -function formatClaimStatus(claimedBy: string | null) { - return claimedBy ? `Claimed by <@${claimedBy}>` : "Unclaimed"; -} - function readCloseReason(interaction: APIModalSubmitInteraction) { for (const component of interaction.data.components) { if (!("components" in component)) { @@ -623,8 +621,8 @@ function readCloseReason(interaction: APIModalSubmitInteraction) { return null; } -function normalizeCloseReason(reason: string | null) { - return reason?.trim() || "No reason provided."; +function normalizeCloseReason(app: BotApp, reason: string | null) { + return reason?.trim() || getDefaultNoReason(app); } /* diff --git a/src/features/tickets/messages.ts b/src/features/tickets/messages.ts index 398a4898..fd10d78a 100644 --- a/src/features/tickets/messages.ts +++ b/src/features/tickets/messages.ts @@ -19,8 +19,9 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import type { APIButtonComponentWithCustomId, APIMessageTopLevelComponent } from "@discordjs/core"; import { ComponentType, MessageFlags } from "@discordjs/core"; import { MESSAGE_TEMPLATES_DIRECTORY } from "@/features/tickets/constants"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateSource } from "@/features/tickets/types"; import { renderTemplate } from "@/features/tickets/utils"; +import type { BotApp } from "@/core/types"; const TEMPLATE_SLOT_TYPE = "template-slot"; const TEMPLATE_SLOT_KIND_MANY = "many"; @@ -51,12 +52,20 @@ export function createRuntimeTextSlot() { } export async function loadMessageTemplate( + app: BotApp, reference: string, tokens?: Record ): Promise { const resolvedPath = await resolveMessageTemplatePath(reference); const rawPayload = await loadMessageTemplateSource(resolvedPath); - const normalizedPayload = normalizeMessageTemplate(rawPayload); + const templatePayload = + typeof rawPayload === "function" + ? rawPayload({ + locale: app.locale, + LL: app.LL + }) + : rawPayload; + const normalizedPayload = normalizeMessageTemplate(templatePayload); const renderedPayload = tokens ? (renderDeep(normalizedPayload, tokens) as LoadedMessageTemplate) : (structuredClone(normalizedPayload) as LoadedMessageTemplate); @@ -234,7 +243,7 @@ async function loadMessageTemplateSource(filePath: string) { // Templates stay code-only in v4 so they remain typed and can opt into // Components V2 without extra parsing layers. const importedModule = await import(pathToFileURL(filePath).href); - return importedModule.default ?? importedModule.message ?? importedModule; + return (importedModule.default ?? importedModule.message ?? importedModule) as MessageTemplateSource; } throw new Error(`Unsupported template file type "${extension}". Only TypeScript templates are supported.`); diff --git a/src/features/tickets/panel-sync.ts b/src/features/tickets/panel-sync.ts index 35da359a..aa9220a0 100644 --- a/src/features/tickets/panel-sync.ts +++ b/src/features/tickets/panel-sync.ts @@ -65,7 +65,7 @@ export async function handleOpenPanelSelector(context: ComponentExecutionContext if (options.length === 0) { await reply(context.app, interaction, { - content: "You do not have access to any ticket types on this panel.", + content: context.app.LL.tickets.panel.no_visible_types(), flags: MessageFlags.Ephemeral }); return; @@ -73,7 +73,7 @@ export async function handleOpenPanelSelector(context: ComponentExecutionContext await reply(context.app, interaction, { flags: MessageFlags.Ephemeral, - components: [createSelectRow(panelKey, panel.opener, options)] + components: [createSelectRow(context.app, panelKey, panel.opener, options)] }); } @@ -100,7 +100,7 @@ export async function handlePanelSelect(context: ComponentExecutionContext, inte if (!ticketTypeKey) { await reply(context.app, interaction, { - content: "Please select a ticket type.", + content: context.app.LL.tickets.panel.select_type(), flags: MessageFlags.Ephemeral }); return; @@ -110,7 +110,7 @@ export async function handlePanelSelect(context: ComponentExecutionContext, inte if (!allowedTicketTypes.has(ticketTypeKey)) { await reply(context.app, interaction, { - content: "That ticket type is not available from this panel.", + content: context.app.LL.tickets.panel.unavailable_type(), flags: MessageFlags.Ephemeral }); return; @@ -161,7 +161,7 @@ async function recreatePanelMessage( } async function buildPanelMessage(app: BotApp, panelKey: string, panel: PanelConfig) { - const messageTemplate = await loadMessageTemplate(panel.message); + const messageTemplate = await loadMessageTemplate(app, panel.message); const withConfiguredText = appendMessageText(messageTemplate, panel.content); const body = placePanelOpener(withConfiguredText, buildPanelComponents(app, panelKey, panel)); @@ -235,7 +235,7 @@ function isPanelOpenerSlot(value: unknown): value is { function buildPanelComponents(app: BotApp, panelKey: string, panel: PanelConfig): APIMessageTopLevelComponent[] { switch (panel.opener.type) { case "inline-select": - return [createSelectRow(panelKey, panel.opener, buildSelectOptions(app, panel.opener.ticketTypes))]; + return [createSelectRow(app, panelKey, panel.opener, buildSelectOptions(app, panel.opener.ticketTypes))]; case "button-select": return [createButtonRow(panelKey, panel.opener)]; case "buttons": @@ -244,6 +244,7 @@ function buildPanelComponents(app: BotApp, panelKey: string, panel: PanelConfig) } function createSelectRow( + app: BotApp, panelKey: string, opener: Extract, options: APIStringSelectComponent["options"] @@ -254,7 +255,7 @@ function createSelectRow( { type: ComponentType.StringSelect, custom_id: createCustomId("tickets", "panel-select", panelKey), - placeholder: opener.placeholder ?? "Select a ticket type", + placeholder: opener.placeholder ?? app.LL.tickets.panel.select_placeholder(), min_values: 1, max_values: 1, options diff --git a/src/features/tickets/records.ts b/src/features/tickets/records.ts index 8348961d..52b7b586 100644 --- a/src/features/tickets/records.ts +++ b/src/features/tickets/records.ts @@ -31,7 +31,7 @@ export async function getOpenTicketByChannel(app: BotApp, channelId: string | un if (!channelId) { return { ok: false as const, - message: "This interaction was not used in a ticket channel." + message: app.LL.tickets.records.not_ticket_channel() }; } @@ -40,14 +40,14 @@ export async function getOpenTicketByChannel(app: BotApp, channelId: string | un if (!ticket) { return { ok: false as const, - message: "This channel is not an open ticket." + message: app.LL.tickets.records.not_open_ticket() }; } if (ticket.closedAt) { return { ok: false as const, - message: "This ticket is already closed." + message: app.LL.tickets.records.already_closed() }; } diff --git a/src/features/tickets/text.ts b/src/features/tickets/text.ts new file mode 100644 index 00000000..360f2e52 --- /dev/null +++ b/src/features/tickets/text.ts @@ -0,0 +1,15 @@ +import type { BotApp } from "@/core/types"; + +export function getDefaultNoReason(app: BotApp) { + return app.LL.shared.no_reason_provided(); +} + +export function formatClaimStatus(app: BotApp, claimedBy: string | null | undefined) { + return claimedBy ? app.LL.shared.claim_status.claimed_by({ userId: claimedBy }) : app.LL.shared.claim_status.unclaimed(); +} + +export function formatTranscriptStatus(app: BotApp, transcriptUrl: string | null | undefined) { + return transcriptUrl + ? app.LL.shared.transcript_status.ready({ url: transcriptUrl }) + : app.LL.shared.transcript_status.unavailable(); +} diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index b26bf461..37bbcb37 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -44,13 +44,14 @@ import { getTicketType, userCanAccessTicketType } from "@/features/tickets/config-access"; -import { DEFAULT_NO_REASON, TICKET_ACCESS_ALLOW } from "@/features/tickets/constants"; +import { TICKET_ACCESS_ALLOW } from "@/features/tickets/constants"; import { appendMessageText, finalizeMessageTemplate, hasMessageComponentCustomId, loadMessageTemplate } from "@/features/tickets/messages"; +import { formatClaimStatus, getDefaultNoReason } from "@/features/tickets/text"; import type { LoadedMessageTemplate, TicketOpenContext, @@ -75,7 +76,8 @@ export async function handleOpenFormSubmit(context: ComponentExecutionContext, i const ticketType = getTicketType(context.app, ticketTypeKey); const questions = ticketType.openForm?.questions ?? []; const answers = extractSubmittedValues(interaction); - const reason = questions.length > 0 ? createTicketOpenReason(questions, answers) : createDefaultTicketOpenReason(); + const reason = + questions.length > 0 ? createTicketOpenReason(context.app, questions, answers) : createDefaultTicketOpenReason(context.app); await createTicket(context.app, interaction, ticketTypeKey, ticketType, reason); } @@ -87,7 +89,7 @@ export async function continueTicketOpen(app: BotApp, interaction: APIMessageCom if (!userCanAccessTicketType(app, ticketType, roleIds)) { await reply(app, interaction, { - content: "You are not allowed to create that ticket type.", + content: app.LL.tickets.open.not_allowed_type(), flags: MessageFlags.Ephemeral }); return; @@ -98,7 +100,7 @@ export async function continueTicketOpen(app: BotApp, interaction: APIMessageCom if (!allowedTypes.has(context.ticketTypeKey)) { await reply(app, interaction, { - content: "That ticket type is not available from this panel.", + content: app.LL.tickets.open.unavailable_type(), flags: MessageFlags.Ephemeral }); return; @@ -109,7 +111,7 @@ export async function continueTicketOpen(app: BotApp, interaction: APIMessageCom if (app.config.tickets.maxOpenPerUser > 0 && currentOpenCount >= app.config.tickets.maxOpenPerUser) { await reply(app, interaction, { - content: `You already have the maximum number of open tickets (${app.config.tickets.maxOpenPerUser}).`, + content: app.LL.tickets.open.max_open_reached({ limit: app.config.tickets.maxOpenPerUser }), flags: MessageFlags.Ephemeral }); return; @@ -130,13 +132,13 @@ export async function continueTicketOpen(app: BotApp, interaction: APIMessageCom if (interaction.data.component_type === ComponentType.StringSelect) { // Update the open panel message so that it resets the selection of the user, letting them open another ticket later. await updateMessage(app, interaction, {}); - await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason(), { + await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason(app), { responseMode: "follow-up" }); return; } - await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason()); + await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason(app)); } async function createTicket( @@ -173,6 +175,7 @@ async function createTicket( permission_overwrites: buildTicketPermissionOverwrites(app, user.id, ticketType) }); const tokens = createTicketRenderTokens({ + app, channelId: channel.id, createdByMention: `<@${user.id}>`, openReason: reason, @@ -213,7 +216,7 @@ async function createTicket( }); const successMessage = { - content: `Your ticket has been created: <#${channel.id}>`, + content: app.LL.tickets.open.created({ channelId: channel.id }), flags: MessageFlags.Ephemeral }; @@ -261,7 +264,7 @@ export async function buildTicketWelcomeMessage( closeButtonCustomId, staffMentions: roleMentions.length ? ` ${roleMentions.join(" ")}` : "" }; - const messageTemplate = messageReference ? await loadMessageTemplate(messageReference, renderedTokens) : {}; + const messageTemplate = messageReference ? await loadMessageTemplate(app, messageReference, renderedTokens) : {}; const configuredContent = ticketType.welcomeContent ?? app.config.tickets.defaultWelcomeContent; const runtimeText = configuredContent ? renderTemplate(configuredContent, renderedTokens) : undefined; const withRuntimeText = appendMessageText(messageTemplate, runtimeText, { slot: "runtime-text" }); @@ -287,12 +290,13 @@ export async function buildTicketWelcomeMessage( export async function syncTicketWelcomeMessage(app: BotApp, ticket: TicketRecord, ticketType = getTicketType(app, ticket.type)) { const creator = await app.client.api.users.get(ticket.createdBy).catch(() => null); const tokens = createTicketRenderTokens({ + app, channelId: ticket.channelId, - claimStatus: formatClaimStatus(ticket.claimedBy), + claimStatus: formatClaimStatus(app, ticket.claimedBy), claimerId: ticket.claimedBy ?? undefined, claimerMention: ticket.claimedBy ? `<@${ticket.claimedBy}>` : undefined, createdByMention: `<@${ticket.createdBy}>`, - openReason: parseStoredTicketOpenReason(ticket.reason), + openReason: parseStoredTicketOpenReason(app, ticket.reason), ticketNumber: ticket.id.toString(), ticketTypeKey: ticket.type, ticketTypeName: ticketType.name, @@ -343,7 +347,7 @@ function buildTicketActionButtons( buttons.push({ type: ComponentType.Button, custom_id: options.closeButtonCustomId, - label: "Close Ticket", + label: app.LL.tickets.actions.close_ticket(), style: ButtonStyle.Danger, disabled: options.disableActions }); @@ -354,14 +358,14 @@ function buildTicketActionButtons( ? ({ type: ComponentType.Button, custom_id: options.unclaimButtonCustomId, - label: "Unclaim Ticket", + label: app.LL.tickets.actions.unclaim_ticket(), style: ButtonStyle.Secondary, disabled: options.disableActions } satisfies APIButtonComponentWithCustomId) : ({ type: ComponentType.Button, custom_id: options.claimButtonCustomId, - label: "Claim Ticket", + label: app.LL.tickets.actions.claim_ticket(), style: ButtonStyle.Primary, disabled: options.disableActions } satisfies APIButtonComponentWithCustomId); @@ -504,23 +508,28 @@ function extractSubmittedValues(interaction: APIModalSubmitInteraction) { return values; } -function createDefaultTicketOpenReason(): TicketOpenReasonData { +function createDefaultTicketOpenReason(app: BotApp): TicketOpenReasonData { return { answers: [], - combined: DEFAULT_NO_REASON + combined: getDefaultNoReason(app) }; } -function createTicketOpenReason(questions: TicketQuestionConfig[], answers: Map): TicketOpenReasonData { - const normalizedAnswers = questions.map((question) => normalizeAnswer(answers.get(question.key))); +function createTicketOpenReason( + app: BotApp, + questions: TicketQuestionConfig[], + answers: Map +): TicketOpenReasonData { + const normalizedAnswers = questions.map((question) => normalizeAnswer(app, answers.get(question.key))); return { answers: normalizedAnswers, - combined: formatQuestionAnswers(questions, normalizedAnswers) + combined: formatQuestionAnswers(app, questions, normalizedAnswers) }; } function createTicketRenderTokens(input: { + app: BotApp; channelId?: string; claimStatus?: string; claimerId?: string; @@ -535,7 +544,7 @@ function createTicketRenderTokens(input: { }) { const tokens: TicketRenderTokens = { channelId: input.channelId, - claimStatus: input.claimStatus ?? formatClaimStatus(input.claimerId ?? null), + claimStatus: input.claimStatus ?? formatClaimStatus(input.app, input.claimerId ?? null), claimerId: input.claimerId, claimerMention: input.claimerMention, createdByMention: input.createdByMention, @@ -556,17 +565,20 @@ function createTicketRenderTokens(input: { return tokens; } -function formatQuestionAnswers(questions: TicketQuestionConfig[], answers: string[]) { - const lines = questions.map((question, index) => `${question.label}: ${answers[index] ?? DEFAULT_NO_REASON}`); - return lines.join("\n"); -} +function formatQuestionAnswers(app: BotApp, questions: TicketQuestionConfig[], answers: string[]): string { + const lines = questions.map((question, index) => { + const answer = answers[index] ?? getDefaultNoReason(app); -function formatClaimStatus(claimedBy: string | null) { - return claimedBy ? `Claimed by <@${claimedBy}>` : "Unclaimed"; + return app.LL.tickets.open.question_answer({ + label: question.label, + answer + }); + }); + return lines.join("\n"); } -function normalizeAnswer(answer: string | undefined) { - return answer?.trim() || DEFAULT_NO_REASON; +function normalizeAnswer(app: BotApp, answer: string | undefined) { + return answer?.trim() || getDefaultNoReason(app); } function serializeTicketOpenReason(reason: TicketOpenReasonData) { @@ -581,9 +593,9 @@ function serializeTicketOpenReason(reason: TicketOpenReasonData) { }); } -function parseStoredTicketOpenReason(reason: string | null | undefined): TicketOpenReasonData { +function parseStoredTicketOpenReason(app: BotApp, reason: string | null | undefined): TicketOpenReasonData { if (!reason) { - return createDefaultTicketOpenReason(); + return createDefaultTicketOpenReason(app); } try { @@ -597,7 +609,7 @@ function parseStoredTicketOpenReason(reason: string | null | undefined): TicketO } return { - answers: parsed.answers.map((answer) => normalizeAnswer(typeof answer === "string" ? answer : undefined)), + answers: parsed.answers.map((answer) => normalizeAnswer(app, typeof answer === "string" ? answer : undefined)), combined: parsed.combined }; } catch { diff --git a/src/features/tickets/transcripts.ts b/src/features/tickets/transcripts.ts index 3d67dbef..863948a9 100644 --- a/src/features/tickets/transcripts.ts +++ b/src/features/tickets/transcripts.ts @@ -55,7 +55,7 @@ export async function startTranscriptJob( } async function createTranscript(app: BotApp, channelId: string, onStatus?: TranscriptStatusHandler) { - await reportStatus(onStatus, "Collecting ticket messages..."); + await reportStatus(onStatus, app.LL.tickets.transcript.collecting_messages()); const [channel, guild, messages] = await Promise.all([ app.client.api.channels.get(channelId), @@ -63,7 +63,7 @@ async function createTranscript(app: BotApp, channelId: string, onStatus?: Trans fetchAllMessages(app, channelId) ]); - await reportStatus(onStatus, "Creating transcript..."); + await reportStatus(onStatus, app.LL.tickets.transcript.creating()); const draftTranscript = await buildEnrichedDiscordApiTranscriptData({ messages, @@ -118,7 +118,7 @@ async function createTranscript(app: BotApp, channelId: string, onStatus?: Trans } }); - await reportStatus(onStatus, "Uploading transcript..."); + await reportStatus(onStatus, app.LL.tickets.transcript.uploading()); const uploadClient = new TicketPmUploadClient({ baseUrl: TRANSCRIPT_BASE_URL, @@ -126,8 +126,8 @@ async function createTranscript(app: BotApp, channelId: string, onStatus?: Trans }); const result = await uploadClient.uploadDraftTranscript(draftTranscript, { uuidStyleIds: app.config.uuidType !== "emoji", - avatarProgress: createProgressHandler("Uploading avatars...", onStatus), - mediaProgress: createProgressHandler("Uploading attachments...", onStatus) + avatarProgress: createProgressHandler(app, app.LL.tickets.transcript.uploading_avatars(), onStatus), + mediaProgress: createProgressHandler(app, app.LL.tickets.transcript.uploading_attachments(), onStatus) }); return `${TRANSCRIPT_VIEW_BASE_URL}${result.id}`; @@ -163,7 +163,7 @@ async function fetchAllMessages(app: BotApp, channelId: string) { return messages.reverse(); } -function createProgressHandler(label: string, onStatus?: TranscriptStatusHandler) { +function createProgressHandler(app: BotApp, label: string, onStatus?: TranscriptStatusHandler) { let lastBucket = -1; return (completed: number, total: number) => { @@ -178,7 +178,13 @@ function createProgressHandler(label: string, onStatus?: TranscriptStatusHandler } lastBucket = bucket; - void onStatus(`${label} (${completed}/${total})`); + void onStatus( + app.LL.tickets.transcript.progress({ + label, + completed, + total + }) + ); }; } diff --git a/src/features/tickets/types.ts b/src/features/tickets/types.ts index 695b41b6..b582ef0d 100644 --- a/src/features/tickets/types.ts +++ b/src/features/tickets/types.ts @@ -14,6 +14,7 @@ This notice must not be removed, obscured, or replaced. */ import type { APIAllowedMentions, APIEmbed, APIMessageTopLevelComponent } from "@discordjs/core"; +import type { Locales, TranslationFunctions } from "../../../i18n/i18n-types.js"; import type { VersionedConfig } from "@/config/index"; export type CurrentConfig = VersionedConfig<"0.0.1">; @@ -37,6 +38,13 @@ export interface LoadedMessageTemplate { useComponentsV2?: boolean; } +export interface MessageTemplateContext { + locale: Locales; + LL: TranslationFunctions; +} + +export type MessageTemplateSource = LoadedMessageTemplate | ((context: MessageTemplateContext) => LoadedMessageTemplate); + export interface TicketOpenContext { ticketTypeKey: string; panelKey?: string; From 330b7e693b37263d9590b1ffa82bcecc13577f47 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:09:08 +0200 Subject: [PATCH 32/67] feat(commands): use localized strings in Slash commands * Translate command descriptions and options * Replace hardcoded messages with localized equivalents * Update global command synchronization script --- src/deploy-commands.ts | 4 ++- src/features/commands/add/command.ts | 19 ++++++------ src/features/commands/claim/command.ts | 6 ++-- src/features/commands/cleardm/command.ts | 13 ++++---- src/features/commands/close/command.ts | 6 ++-- src/features/commands/mass_add/command.ts | 36 +++++++++++++++-------- src/features/commands/remove/command.ts | 21 +++++++------ src/features/commands/rename/command.ts | 15 +++++----- src/features/commands/unclaim/command.ts | 6 ++-- 9 files changed, 73 insertions(+), 53 deletions(-) diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index 505e685a..755fc2d1 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -18,6 +18,7 @@ import { REST } from "@discordjs/rest"; import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord-api-types/v10"; import { config } from "dotenv"; import { discoverCommands, discoverEvents, discoverFeatures } from "@/core/discovery"; +import { createBotI18n } from "@/core/i18n"; import { createLogger, type Logger } from "@/core/logger"; import { createHandlerRegistry } from "@/core/registry"; import botConfig from "../config/config.ts"; @@ -53,7 +54,8 @@ async function deployCommands() { discoverEvents(logger), discoverFeatures(logger) ]); - const registry = createHandlerRegistry({ commands, features, events, logger }); + const i18n = createBotI18n(botConfig.lang, logger); + const registry = createHandlerRegistry({ commands, features, events, logger, LL: i18n.LL }); await deployApplicationCommands({ applicationCommands: registry.applicationCommands, diff --git a/src/features/commands/add/command.ts b/src/features/commands/add/command.ts index b67b48d2..a2e3a0a6 100644 --- a/src/features/commands/add/command.ts +++ b/src/features/commands/add/command.ts @@ -30,24 +30,25 @@ import { getOpenTicketByChannel } from "@/features/tickets/records"; import { getInteractionUser } from "@/features/tickets/utils"; export default defineCommand({ - data: { + data: (LL) => ({ name: "add", - description: "Add someone to the current ticket", + description: LL.commands.add.description(), options: [ { name: "user", - description: "The user to add", + description: LL.commands.add.options.user.description(), required: true, type: ApplicationCommandOptionType.User } ] - }, + }), async execute({ app }, interaction) { + const LL = app.LL; const selectedUser = getUserOption(interaction, "user"); if (!selectedUser) { await reply(app, interaction, { - content: "Choose a user to add to this ticket.", + content: LL.commands.add.choose_user(), flags: MessageFlags.Ephemeral }); return; @@ -67,7 +68,7 @@ export default defineCommand({ if (selectedUser.userId === openTicket.ticket.createdBy) { await reply(app, interaction, { - content: "That user already has access to this ticket.", + content: LL.commands.add.already_has_access(), flags: MessageFlags.Ephemeral }); return; @@ -75,7 +76,7 @@ export default defineCommand({ if (invitedUserIds.includes(selectedUser.userId)) { await reply(app, interaction, { - content: "That user is already invited to this ticket.", + content: LL.commands.add.already_invited(), flags: MessageFlags.Ephemeral }); return; @@ -83,7 +84,7 @@ export default defineCommand({ if (invitedUserIds.length >= MAX_INVITED_TICKET_USERS) { await reply(app, interaction, { - content: `You cannot invite more than ${MAX_INVITED_TICKET_USERS} users to one ticket.`, + content: LL.commands.add.invite_limit_reached({ limit: MAX_INVITED_TICKET_USERS }), flags: MessageFlags.Ephemeral }); return; @@ -99,7 +100,7 @@ export default defineCommand({ }); await reply(app, interaction, { - content: `Added <@${selectedUser.userId}> to this ticket.`, + content: LL.commands.add.success({ userId: selectedUser.userId }), flags: MessageFlags.Ephemeral }); } diff --git a/src/features/commands/claim/command.ts b/src/features/commands/claim/command.ts index 721eddca..9f2ab52e 100644 --- a/src/features/commands/claim/command.ts +++ b/src/features/commands/claim/command.ts @@ -17,10 +17,10 @@ import { defineCommand } from "@/core/defineCommand"; import { executeClaimCommand } from "@/features/tickets/claim-workflow"; export default defineCommand({ - data: { + data: (LL) => ({ name: "claim", - description: "Claim the current ticket" - }, + description: LL.commands.claim.description() + }), execute: executeClaimCommand }); diff --git a/src/features/commands/cleardm/command.ts b/src/features/commands/cleardm/command.ts index 31bb9955..4dbda7b5 100644 --- a/src/features/commands/cleardm/command.ts +++ b/src/features/commands/cleardm/command.ts @@ -19,13 +19,14 @@ import { editReply, reply } from "@/core/respond"; import { getInteractionUser } from "@/features/tickets/utils"; export default defineCommand({ - data: { + data: (LL) => ({ name: "cleardm", - description: "Clear the bot's ticket history from your DMs" - }, + description: LL.commands.cleardm.description() + }), async execute({ app }, interaction) { + const LL = app.LL; await reply(app, interaction, { - content: "Clearing your ticket DM history...", + content: LL.commands.cleardm.starting(), flags: MessageFlags.Ephemeral }); @@ -34,7 +35,7 @@ export default defineCommand({ if (!dmChannel?.id) { await editReply(app, interaction, { - content: "I could not access your DM channel." + content: LL.commands.cleardm.dm_unavailable() }); return; } @@ -69,7 +70,7 @@ export default defineCommand({ } await editReply(app, interaction, { - content: deletedCount > 0 ? `Cleared ${deletedCount} ticket DM messages.` : "No ticket DM messages were found." + content: deletedCount > 0 ? LL.commands.cleardm.cleared({ count: deletedCount }) : LL.commands.cleardm.none_found() }); } }); diff --git a/src/features/commands/close/command.ts b/src/features/commands/close/command.ts index 5010b67c..582f185d 100644 --- a/src/features/commands/close/command.ts +++ b/src/features/commands/close/command.ts @@ -17,10 +17,10 @@ import { defineCommand } from "@/core/defineCommand"; import { executeCloseCommand } from "@/features/tickets/close-workflow"; export default defineCommand({ - data: { + data: (LL) => ({ name: "close", - description: "Close the current ticket" - }, + description: LL.commands.close.description() + }), execute: executeCloseCommand }); diff --git a/src/features/commands/mass_add/command.ts b/src/features/commands/mass_add/command.ts index a3369de4..36c5daf0 100644 --- a/src/features/commands/mass_add/command.ts +++ b/src/features/commands/mass_add/command.ts @@ -30,25 +30,26 @@ import { getOpenTicketByChannel } from "@/features/tickets/records"; import { getInteractionUser } from "@/features/tickets/utils"; export default defineCommand({ - data: { + data: (LL) => ({ name: "mass_add", - description: "Add multiple users to the current ticket", + description: LL.commands.mass_add.description(), options: [ { name: "users", - description: "Comma-separated user IDs or mentions", + description: LL.commands.mass_add.options.users.description(), required: true, type: ApplicationCommandOptionType.String } ] - }, + }), async execute({ app }, interaction) { + const LL = app.LL; const rawValue = getStringOption(interaction, "users"); const requestedUserIds = parseRequestedUserIds(rawValue ?? ""); if (requestedUserIds.length === 0) { await reply(app, interaction, { - content: "Provide at least one user ID or mention.", + content: LL.commands.mass_add.provide_users(), flags: MessageFlags.Ephemeral }); return; @@ -110,7 +111,7 @@ export default defineCommand({ } await reply(app, interaction, { - content: buildMassAddSummary(addedUserIds, skippedUserIds, invalidUserIds, limitReached), + content: buildMassAddSummary(app, addedUserIds, skippedUserIds, invalidUserIds, limitReached), flags: MessageFlags.Ephemeral }); } @@ -136,25 +137,36 @@ function parseRequestedUserIds(rawValue: string) { return [...new Set(requestedUserIds)]; } -function buildMassAddSummary(addedUserIds: string[], skippedUserIds: string[], invalidUserIds: string[], limitReached: boolean) { +function buildMassAddSummary( + app: Parameters[0], + addedUserIds: string[], + skippedUserIds: string[], + invalidUserIds: string[], + limitReached: boolean +) { + const LL = app.LL; const lines: string[] = []; if (addedUserIds.length > 0) { - lines.push(`Added ${addedUserIds.map((userId) => `<@${userId}>`).join(", ")}.`); + lines.push( + LL.commands.mass_add.summary.added({ + mentions: addedUserIds.map((userId) => `<@${userId}>`).join(", ") + }) + ); } else { - lines.push("No users were added."); + lines.push(LL.commands.mass_add.summary.none_added()); } if (skippedUserIds.length > 0) { - lines.push(`Skipped ${skippedUserIds.length} user(s) that already had access.`); + lines.push(LL.commands.mass_add.summary.skipped_existing({ count: skippedUserIds.length })); } if (invalidUserIds.length > 0) { - lines.push(`Skipped ${invalidUserIds.length} invalid user ID(s).`); + lines.push(LL.commands.mass_add.summary.skipped_invalid({ count: invalidUserIds.length })); } if (limitReached) { - lines.push(`Stopped when the ${MAX_INVITED_TICKET_USERS}-user ticket limit was reached.`); + lines.push(LL.commands.mass_add.summary.limit_reached({ limit: MAX_INVITED_TICKET_USERS })); } return lines.join("\n"); diff --git a/src/features/commands/remove/command.ts b/src/features/commands/remove/command.ts index 781a6f58..40fcff8d 100644 --- a/src/features/commands/remove/command.ts +++ b/src/features/commands/remove/command.ts @@ -31,19 +31,20 @@ import { getInteractionUser } from "@/features/tickets/utils"; const REMOVE_USERS_CUSTOM_ID = createCustomId("tickets", "remove-users"); export default defineCommand({ - data: { + data: (LL) => ({ name: "remove", - description: "Remove invited users from the current ticket", + description: LL.commands.remove.description(), options: [ { name: "user", - description: "The invited user to remove immediately", + description: LL.commands.remove.options.user.description(), required: false, type: ApplicationCommandOptionType.User } ] - }, + }), async execute({ app }, interaction) { + const LL = app.LL; const openTicket = await getOpenTicketByChannel(app, interaction.channel_id); if (!openTicket.ok) { @@ -58,7 +59,7 @@ export default defineCommand({ if (invitedUserIds.length === 0) { await reply(app, interaction, { - content: "There are no invited users to remove from this ticket.", + content: LL.commands.remove.no_invited_users(), flags: MessageFlags.Ephemeral }); return; @@ -84,7 +85,7 @@ export default defineCommand({ ); await reply(app, interaction, { - content: "Select the invited users you want to remove from this ticket.", + content: LL.commands.remove.select_users(), flags: MessageFlags.Ephemeral, components: [ { @@ -93,7 +94,7 @@ export default defineCommand({ { type: ComponentType.StringSelect, custom_id: REMOVE_USERS_CUSTOM_ID, - placeholder: "Choose users to remove", + placeholder: LL.commands.remove.select_placeholder(), min_values: 1, max_values: options.length, options @@ -150,7 +151,7 @@ async function removeUsersFromTicket( const removableUserIds = selectedUserIds.filter((userId) => invitedUserIds.includes(userId)); if (removableUserIds.length === 0) { - await respond(app, interaction, "Those users are not invited to this ticket.", options?.responseMode); + await respond(app, interaction, app.LL.commands.remove.not_invited(), options?.responseMode); return; } @@ -178,7 +179,9 @@ async function removeUsersFromTicket( await respond( app, interaction, - `Removed ${removableUserIds.map((userId) => `<@${userId}>`).join(", ")} from this ticket.`, + app.LL.commands.remove.success({ + mentions: removableUserIds.map((userId) => `<@${userId}>`).join(", ") + }), options?.responseMode ); } diff --git a/src/features/commands/rename/command.ts b/src/features/commands/rename/command.ts index 1ae3f353..2adf7f3c 100644 --- a/src/features/commands/rename/command.ts +++ b/src/features/commands/rename/command.ts @@ -25,19 +25,20 @@ import { getOpenTicketByChannel } from "@/features/tickets/records"; import { getInteractionUser, getMemberRoleIds, sanitizeChannelName } from "@/features/tickets/utils"; export default defineCommand({ - data: { + data: (LL) => ({ name: "rename", - description: "Rename the current ticket", + description: LL.commands.rename.description(), options: [ { name: "name", - description: "The new ticket channel name", + description: LL.commands.rename.options.name.description(), required: true, type: ApplicationCommandOptionType.String } ] - }, + }), async execute({ app }, interaction) { + const LL = app.LL; const openTicket = await getOpenTicketByChannel(app, interaction.channel_id); if (!openTicket.ok) { @@ -50,7 +51,7 @@ export default defineCommand({ if (!hasTicketStaffAccess(app, openTicket.ticketType, getMemberRoleIds(interaction))) { await reply(app, interaction, { - content: "Only staff can rename this ticket.", + content: LL.commands.rename.only_staff(), flags: MessageFlags.Ephemeral }); return; @@ -60,7 +61,7 @@ export default defineCommand({ if (!requestedName) { await reply(app, interaction, { - content: "Provide a new ticket name.", + content: LL.commands.rename.provide_name(), flags: MessageFlags.Ephemeral }); return; @@ -94,7 +95,7 @@ export default defineCommand({ } await reply(app, interaction, { - content: `Ticket renamed to <#${openTicket.ticket.channelId}>.`, + content: LL.commands.rename.success({ channelId: openTicket.ticket.channelId }), flags: MessageFlags.Ephemeral }); } diff --git a/src/features/commands/unclaim/command.ts b/src/features/commands/unclaim/command.ts index 621c5a48..fdc25487 100644 --- a/src/features/commands/unclaim/command.ts +++ b/src/features/commands/unclaim/command.ts @@ -17,10 +17,10 @@ import { defineCommand } from "@/core/defineCommand"; import { executeUnclaimCommand } from "@/features/tickets/claim-workflow"; export default defineCommand({ - data: { + data: (LL) => ({ name: "unclaim", - description: "Unclaim the current ticket" - }, + description: LL.commands.unclaim.description() + }), execute: executeUnclaimCommand }); From fb51414d7c07cc3cc4eef206d8969dece48e63a8 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:10:49 +0200 Subject: [PATCH 33/67] refactor(messages): migrate templates to dynamic i18n functions * Transform exported templates into context-aware functions * Apply dictionary-based translation to static text elements --- messages/logs/ticket-claimed.ts | 23 ++++++++++++----- messages/logs/ticket-closed.ts | 27 +++++++++++++++----- messages/logs/ticket-created.ts | 25 +++++++++++++----- messages/logs/ticket-deleted.ts | 27 +++++++++++++++----- messages/logs/ticket-renamed.ts | 25 +++++++++++++----- messages/logs/ticket-unclaimed.ts | 23 ++++++++++++----- messages/logs/user-added.ts | 23 ++++++++++++----- messages/logs/user-removed.ts | 23 ++++++++++++----- messages/tickets/open-panel.ts | 26 ++++--------------- messages/tickets/ticket-closed-billing.ts | 22 +++++++++------- messages/tickets/ticket-closed-dm-billing.ts | 20 +++++++++------ messages/tickets/ticket-closed-dm-general.ts | 20 +++++++++------ messages/tickets/ticket-closed-dm-report.ts | 22 +++++++++------- messages/tickets/ticket-closed-dm.ts | 16 +++++++----- messages/tickets/ticket-closed-general.ts | 24 +++++++++-------- messages/tickets/ticket-closed-report.ts | 24 +++++++++-------- messages/tickets/ticket-closed.ts | 20 +++++++++------ messages/tickets/ticket-opened-billing.ts | 18 ++++++------- messages/tickets/ticket-opened-general.ts | 18 ++++++------- messages/tickets/ticket-opened-report.ts | 18 ++++++------- messages/tickets/ticket-opened.ts | 14 +++++----- 21 files changed, 283 insertions(+), 175 deletions(-) diff --git a/messages/logs/ticket-claimed.ts b/messages/logs/ticket-claimed.ts index a90ada93..a2ff3747 100644 --- a/messages/logs/ticket-claimed.ts +++ b/messages/logs/ticket-claimed.ts @@ -14,24 +14,35 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketClaimedLogMessage: LoadedMessageTemplate = { +const ticketClaimedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, accent_color: 16426522, components: [ - { type: ComponentType.TextDisplay, content: "## Ticket Claimed" }, - { type: ComponentType.TextDisplay, content: "{actorMention} claimed {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_claimed.title() }, { type: ComponentType.TextDisplay, - content: "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Open Age**: {ticketAge}" + content: LL.logs.templates.ticket_claimed.action({ + actorMention: "{actorMention}", + ticketChannelMention: "{ticketChannelMention}" + }) + }, + { + type: ComponentType.TextDisplay, + content: LL.logs.templates.ticket_claimed.details({ + ticketId: "{ticketId}", + ticketTypeName: "{ticketTypeName}", + createdByMention: "{createdByMention}", + ticketAge: "{ticketAge}" + }) } ] } ] -}; +}); export default ticketClaimedLogMessage; diff --git a/messages/logs/ticket-closed.ts b/messages/logs/ticket-closed.ts index 4a0ed8fd..4ef607ae 100644 --- a/messages/logs/ticket-closed.ts +++ b/messages/logs/ticket-closed.ts @@ -14,25 +14,38 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketClosedLogMessage: LoadedMessageTemplate = { +const ticketClosedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, accent_color: 16007990, components: [ - { type: ComponentType.TextDisplay, content: "## Ticket Closed" }, - { type: ComponentType.TextDisplay, content: "{actorMention} closed {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_closed.title() }, { type: ComponentType.TextDisplay, - content: - "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Claim Status**: {claimStatus}\n**Open Age**: {ticketAge}\n**Reason**: {reason}\n**Transcript**: {transcriptStatus}" + content: LL.logs.templates.ticket_closed.action({ + actorMention: "{actorMention}", + ticketChannelMention: "{ticketChannelMention}" + }) + }, + { + type: ComponentType.TextDisplay, + content: LL.logs.templates.ticket_closed.details({ + ticketId: "{ticketId}", + ticketTypeName: "{ticketTypeName}", + createdByMention: "{createdByMention}", + claimStatus: "{claimStatus}", + ticketAge: "{ticketAge}", + reason: "{reason}", + transcriptStatus: "{transcriptStatus}" + }) } ] } ] -}; +}); export default ticketClosedLogMessage; diff --git a/messages/logs/ticket-created.ts b/messages/logs/ticket-created.ts index 7b7cefcb..8706e624 100644 --- a/messages/logs/ticket-created.ts +++ b/messages/logs/ticket-created.ts @@ -14,25 +14,36 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketCreatedLogMessage: LoadedMessageTemplate = { +const ticketCreatedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, accent_color: 3901635, components: [ - { type: ComponentType.TextDisplay, content: "## Ticket Created" }, - { type: ComponentType.TextDisplay, content: "{actorMention} opened {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_created.title() }, { type: ComponentType.TextDisplay, - content: - "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Created**: {createdAt}\n**Reason**: {reason}" + content: LL.logs.templates.ticket_created.action({ + actorMention: "{actorMention}", + ticketChannelMention: "{ticketChannelMention}" + }) + }, + { + type: ComponentType.TextDisplay, + content: LL.logs.templates.ticket_created.details({ + ticketId: "{ticketId}", + ticketTypeName: "{ticketTypeName}", + createdByMention: "{createdByMention}", + createdAt: "{createdAt}", + reason: "{reason}" + }) } ] } ] -}; +}); export default ticketCreatedLogMessage; diff --git a/messages/logs/ticket-deleted.ts b/messages/logs/ticket-deleted.ts index f389e533..d86795ca 100644 --- a/messages/logs/ticket-deleted.ts +++ b/messages/logs/ticket-deleted.ts @@ -14,25 +14,38 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketDeletedLogMessage: LoadedMessageTemplate = { +const ticketDeletedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, accent_color: 13632027, components: [ - { type: ComponentType.TextDisplay, content: "## Ticket Deleted" }, - { type: ComponentType.TextDisplay, content: "{actorMention} deleted {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_deleted.title() }, { type: ComponentType.TextDisplay, - content: - "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Claim Status**: {claimStatus}\n**Open Age**: {ticketAge}\n**Close Reason**: {reason}\n**Transcript**: {transcriptStatus}" + content: LL.logs.templates.ticket_deleted.action({ + actorMention: "{actorMention}", + ticketChannelMention: "{ticketChannelMention}" + }) + }, + { + type: ComponentType.TextDisplay, + content: LL.logs.templates.ticket_deleted.details({ + ticketId: "{ticketId}", + ticketTypeName: "{ticketTypeName}", + createdByMention: "{createdByMention}", + claimStatus: "{claimStatus}", + ticketAge: "{ticketAge}", + reason: "{reason}", + transcriptStatus: "{transcriptStatus}" + }) } ] } ] -}; +}); export default ticketDeletedLogMessage; diff --git a/messages/logs/ticket-renamed.ts b/messages/logs/ticket-renamed.ts index 6fe02ce5..51c3fa48 100644 --- a/messages/logs/ticket-renamed.ts +++ b/messages/logs/ticket-renamed.ts @@ -14,25 +14,36 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketRenamedLogMessage: LoadedMessageTemplate = { +const ticketRenamedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, accent_color: 3447003, components: [ - { type: ComponentType.TextDisplay, content: "## Ticket Renamed" }, - { type: ComponentType.TextDisplay, content: "{actorMention} renamed {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_renamed.title() }, { type: ComponentType.TextDisplay, - content: - "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**From**: `{oldChannelName}`\n**To**: `{newChannelName}`" + content: LL.logs.templates.ticket_renamed.action({ + actorMention: "{actorMention}", + ticketChannelMention: "{ticketChannelMention}" + }) + }, + { + type: ComponentType.TextDisplay, + content: LL.logs.templates.ticket_renamed.details({ + ticketId: "{ticketId}", + ticketTypeName: "{ticketTypeName}", + createdByMention: "{createdByMention}", + oldChannelName: "{oldChannelName}", + newChannelName: "{newChannelName}" + }) } ] } ] -}; +}); export default ticketRenamedLogMessage; diff --git a/messages/logs/ticket-unclaimed.ts b/messages/logs/ticket-unclaimed.ts index 2d9bced2..9e126eac 100644 --- a/messages/logs/ticket-unclaimed.ts +++ b/messages/logs/ticket-unclaimed.ts @@ -14,24 +14,35 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketUnclaimedLogMessage: LoadedMessageTemplate = { +const ticketUnclaimedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, accent_color: 9807270, components: [ - { type: ComponentType.TextDisplay, content: "## Ticket Unclaimed" }, - { type: ComponentType.TextDisplay, content: "{actorMention} unclaimed {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_unclaimed.title() }, { type: ComponentType.TextDisplay, - content: "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}\n**Open Age**: {ticketAge}" + content: LL.logs.templates.ticket_unclaimed.action({ + actorMention: "{actorMention}", + ticketChannelMention: "{ticketChannelMention}" + }) + }, + { + type: ComponentType.TextDisplay, + content: LL.logs.templates.ticket_unclaimed.details({ + ticketId: "{ticketId}", + ticketTypeName: "{ticketTypeName}", + createdByMention: "{createdByMention}", + ticketAge: "{ticketAge}" + }) } ] } ] -}; +}); export default ticketUnclaimedLogMessage; diff --git a/messages/logs/user-added.ts b/messages/logs/user-added.ts index ae439dbc..56fb2e0d 100644 --- a/messages/logs/user-added.ts +++ b/messages/logs/user-added.ts @@ -14,24 +14,35 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const userAddedLogMessage: LoadedMessageTemplate = { +const userAddedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, accent_color: 3901635, components: [ - { type: ComponentType.TextDisplay, content: "## User Added" }, - { type: ComponentType.TextDisplay, content: "{actorMention} added {targetMention} to {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: LL.logs.templates.user_added.title() }, { type: ComponentType.TextDisplay, - content: "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}" + content: LL.logs.templates.user_added.action({ + actorMention: "{actorMention}", + targetMention: "{targetMention}", + ticketChannelMention: "{ticketChannelMention}" + }) + }, + { + type: ComponentType.TextDisplay, + content: LL.logs.templates.user_added.details({ + ticketId: "{ticketId}", + ticketTypeName: "{ticketTypeName}", + createdByMention: "{createdByMention}" + }) } ] } ] -}; +}); export default userAddedLogMessage; diff --git a/messages/logs/user-removed.ts b/messages/logs/user-removed.ts index ce21e947..51ef287d 100644 --- a/messages/logs/user-removed.ts +++ b/messages/logs/user-removed.ts @@ -14,24 +14,35 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const userRemovedLogMessage: LoadedMessageTemplate = { +const userRemovedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, accent_color: 16007990, components: [ - { type: ComponentType.TextDisplay, content: "## User Removed" }, - { type: ComponentType.TextDisplay, content: "{actorMention} removed {targetMention} from {ticketChannelMention}." }, + { type: ComponentType.TextDisplay, content: LL.logs.templates.user_removed.title() }, { type: ComponentType.TextDisplay, - content: "**Ticket**: #{ticketId} • {ticketTypeName}\n**Opened By**: {createdByMention}" + content: LL.logs.templates.user_removed.action({ + actorMention: "{actorMention}", + targetMention: "{targetMention}", + ticketChannelMention: "{ticketChannelMention}" + }) + }, + { + type: ComponentType.TextDisplay, + content: LL.logs.templates.user_removed.details({ + ticketId: "{ticketId}", + ticketTypeName: "{ticketTypeName}", + createdByMention: "{createdByMention}" + }) } ] } ] -}; +}); export default userRemovedLogMessage; diff --git a/messages/tickets/open-panel.ts b/messages/tickets/open-panel.ts index a578f09e..cf4f61d7 100644 --- a/messages/tickets/open-panel.ts +++ b/messages/tickets/open-panel.ts @@ -15,25 +15,9 @@ This notice must not be removed, obscured, or replaced. import { ComponentType } from "discord-api-types/v10"; import { createPanelOpenerSlot } from "@/features/tickets/messages"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -// Classic embed + components version: - -// const openPanelMessage: LoadedMessageTemplate = { -// useComponentsV2: false, -// embeds: [ -// { -// title: "Open a Ticket", -// description: "Choose the category that matches your request and the bot will create a private ticket for you.", -// color: 16106539 -// } -// ], -// components: [createPanelOpenerSlot()] -// }; - -// Components V2 version: - -const openPanelMessage: LoadedMessageTemplate = { +const openPanelMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ useComponentsV2: true, components: [ { @@ -42,11 +26,11 @@ const openPanelMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, - content: "## Open a Ticket" + content: LL.tickets.templates.open_panel.title() }, { type: ComponentType.TextDisplay, - content: "Choose the category that matches your request and the bot will create a private ticket for you." + content: LL.tickets.templates.open_panel.description() }, { type: ComponentType.Separator @@ -60,7 +44,7 @@ const openPanelMessage: LoadedMessageTemplate = { ] } ] -}; +}); export default openPanelMessage; diff --git a/messages/tickets/ticket-closed-billing.ts b/messages/tickets/ticket-closed-billing.ts index 0673d51e..b258c6be 100644 --- a/messages/tickets/ticket-closed-billing.ts +++ b/messages/tickets/ticket-closed-billing.ts @@ -14,9 +14,9 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const billingTicketClosedMessage: LoadedMessageTemplate = { +const ticketClosedBillingMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, @@ -24,19 +24,23 @@ const billingTicketClosedMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, - content: "## Billing Ticket Closed" + content: LL.tickets.templates.ticket_closed_billing.title() }, { type: ComponentType.TextDisplay, - content: "<@{userId}>'s billing ticket has been closed." + content: LL.tickets.templates.ticket_closed_billing.subtitle({ userId: "{userId}" }) }, { type: ComponentType.TextDisplay, - content: "**Close Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" + content: LL.tickets.templates.ticket_closed_billing.details({ + reason: "{reason}", + claimStatus: "{claimStatus}", + transcriptStatus: "{transcriptStatus}" + }) }, { type: ComponentType.TextDisplay, - content: "-# _Closed by {closerName}_" + content: LL.tickets.templates.ticket_closed_billing.closed_by({ closerName: "{closerName}" }) }, { type: ComponentType.ActionRow, @@ -44,7 +48,7 @@ const billingTicketClosedMessage: LoadedMessageTemplate = { { type: ComponentType.Button, custom_id: "{deleteButtonCustomId}", - label: "Delete Ticket", + label: LL.tickets.actions.delete_ticket(), style: 4 } ] @@ -52,9 +56,9 @@ const billingTicketClosedMessage: LoadedMessageTemplate = { ] } ] -}; +}); -export default billingTicketClosedMessage; +export default ticketClosedBillingMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-closed-dm-billing.ts b/messages/tickets/ticket-closed-dm-billing.ts index b80f8171..d6baad42 100644 --- a/messages/tickets/ticket-closed-dm-billing.ts +++ b/messages/tickets/ticket-closed-dm-billing.ts @@ -14,9 +14,9 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const billingTicketClosedDmMessage: LoadedMessageTemplate = { +const ticketClosedDmBillingMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, @@ -24,26 +24,30 @@ const billingTicketClosedDmMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, - content: "## Your billing ticket has been closed" + content: LL.tickets.templates.ticket_closed_dm_billing.title() }, { type: ComponentType.TextDisplay, - content: "If you still need help, open a new billing ticket and include your order details again." + content: LL.tickets.templates.ticket_closed_dm_billing.intro() }, { type: ComponentType.TextDisplay, - content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" + content: LL.tickets.templates.ticket_closed_dm_billing.details({ + reason: "{reason}", + claimStatus: "{claimStatus}", + transcriptStatus: "{transcriptStatus}" + }) }, { type: ComponentType.TextDisplay, - content: "-# _Closed by {closerName}_" + content: LL.tickets.templates.ticket_closed_dm_billing.closed_by({ closerName: "{closerName}" }) } ] } ] -}; +}); -export default billingTicketClosedDmMessage; +export default ticketClosedDmBillingMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-closed-dm-general.ts b/messages/tickets/ticket-closed-dm-general.ts index 0bbe1539..b4255703 100644 --- a/messages/tickets/ticket-closed-dm-general.ts +++ b/messages/tickets/ticket-closed-dm-general.ts @@ -14,32 +14,36 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const generalTicketClosedDmMessage: LoadedMessageTemplate = { +const ticketClosedDmGeneralMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, - accent_color: 3447003, + accent_color: 16106539, components: [ { type: ComponentType.TextDisplay, - content: "## Your general support ticket has been closed" + content: LL.tickets.templates.ticket_closed_dm_general.title() }, { type: ComponentType.TextDisplay, - content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" + content: LL.tickets.templates.ticket_closed_dm_general.details({ + reason: "{reason}", + claimStatus: "{claimStatus}", + transcriptStatus: "{transcriptStatus}" + }) }, { type: ComponentType.TextDisplay, - content: "-# _Closed by {closerName}_" + content: LL.tickets.templates.ticket_closed_dm_general.closed_by({ closerName: "{closerName}" }) } ] } ] -}; +}); -export default generalTicketClosedDmMessage; +export default ticketClosedDmGeneralMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-closed-dm-report.ts b/messages/tickets/ticket-closed-dm-report.ts index dd266789..0c878d95 100644 --- a/messages/tickets/ticket-closed-dm-report.ts +++ b/messages/tickets/ticket-closed-dm-report.ts @@ -14,36 +14,40 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const reportTicketClosedDmMessage: LoadedMessageTemplate = { +const ticketClosedDmReportMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, - accent_color: 15158332, + accent_color: 16106539, components: [ { type: ComponentType.TextDisplay, - content: "## Your report ticket has been closed" + content: LL.tickets.templates.ticket_closed_dm_report.title() }, { type: ComponentType.TextDisplay, - content: "Staff reviewed the report and any attached evidence." + content: LL.tickets.templates.ticket_closed_dm_report.intro() }, { type: ComponentType.TextDisplay, - content: "**Resolution Note**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" + content: LL.tickets.templates.ticket_closed_dm_report.details({ + reason: "{reason}", + claimStatus: "{claimStatus}", + transcriptStatus: "{transcriptStatus}" + }) }, { type: ComponentType.TextDisplay, - content: "-# _Closed by {closerName}_" + content: LL.tickets.templates.ticket_closed_dm_report.closed_by({ closerName: "{closerName}" }) } ] } ] -}; +}); -export default reportTicketClosedDmMessage; +export default ticketClosedDmReportMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-closed-dm.ts b/messages/tickets/ticket-closed-dm.ts index efc0d894..848a4c45 100644 --- a/messages/tickets/ticket-closed-dm.ts +++ b/messages/tickets/ticket-closed-dm.ts @@ -14,9 +14,9 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketClosedDmMessage: LoadedMessageTemplate = { +const ticketClosedDmMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, @@ -24,20 +24,24 @@ const ticketClosedDmMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, - content: "## Your ticket has been closed" + content: LL.tickets.templates.ticket_closed_dm.title() }, { type: ComponentType.TextDisplay, - content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" + content: LL.tickets.templates.ticket_closed_dm.details({ + reason: "{reason}", + claimStatus: "{claimStatus}", + transcriptStatus: "{transcriptStatus}" + }) }, { type: ComponentType.TextDisplay, - content: "-# _Closed by {closerName}_" + content: LL.tickets.templates.ticket_closed_dm.closed_by({ closerName: "{closerName}" }) } ] } ] -}; +}); export default ticketClosedDmMessage; diff --git a/messages/tickets/ticket-closed-general.ts b/messages/tickets/ticket-closed-general.ts index a005fc62..a7a60123 100644 --- a/messages/tickets/ticket-closed-general.ts +++ b/messages/tickets/ticket-closed-general.ts @@ -14,29 +14,33 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const generalTicketClosedMessage: LoadedMessageTemplate = { +const ticketClosedGeneralMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, - accent_color: 3447003, + accent_color: 16106539, components: [ { type: ComponentType.TextDisplay, - content: "## General Support Closed" + content: LL.tickets.templates.ticket_closed_general.title() }, { type: ComponentType.TextDisplay, - content: "<@{userId}>'s general support ticket is now closed." + content: LL.tickets.templates.ticket_closed_general.subtitle({ userId: "{userId}" }) }, { type: ComponentType.TextDisplay, - content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" + content: LL.tickets.templates.ticket_closed_general.details({ + reason: "{reason}", + claimStatus: "{claimStatus}", + transcriptStatus: "{transcriptStatus}" + }) }, { type: ComponentType.TextDisplay, - content: "-# _Closed by {closerName}_" + content: LL.tickets.templates.ticket_closed_general.closed_by({ closerName: "{closerName}" }) }, { type: ComponentType.ActionRow, @@ -44,7 +48,7 @@ const generalTicketClosedMessage: LoadedMessageTemplate = { { type: ComponentType.Button, custom_id: "{deleteButtonCustomId}", - label: "Delete Ticket", + label: LL.tickets.actions.delete_ticket(), style: 4 } ] @@ -52,9 +56,9 @@ const generalTicketClosedMessage: LoadedMessageTemplate = { ] } ] -}; +}); -export default generalTicketClosedMessage; +export default ticketClosedGeneralMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-closed-report.ts b/messages/tickets/ticket-closed-report.ts index 26d92f45..4e821a6d 100644 --- a/messages/tickets/ticket-closed-report.ts +++ b/messages/tickets/ticket-closed-report.ts @@ -14,29 +14,33 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const reportTicketClosedMessage: LoadedMessageTemplate = { +const ticketClosedReportMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, - accent_color: 15158332, + accent_color: 16106539, components: [ { type: ComponentType.TextDisplay, - content: "## Report Case Closed" + content: LL.tickets.templates.ticket_closed_report.title() }, { type: ComponentType.TextDisplay, - content: "The report opened by <@{userId}> has been closed." + content: LL.tickets.templates.ticket_closed_report.subtitle({ userId: "{userId}" }) }, { type: ComponentType.TextDisplay, - content: "**Resolution Note**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" + content: LL.tickets.templates.ticket_closed_report.details({ + reason: "{reason}", + claimStatus: "{claimStatus}", + transcriptStatus: "{transcriptStatus}" + }) }, { type: ComponentType.TextDisplay, - content: "-# _Closed by {closerName}_" + content: LL.tickets.templates.ticket_closed_report.closed_by({ closerName: "{closerName}" }) }, { type: ComponentType.ActionRow, @@ -44,7 +48,7 @@ const reportTicketClosedMessage: LoadedMessageTemplate = { { type: ComponentType.Button, custom_id: "{deleteButtonCustomId}", - label: "Delete Ticket", + label: LL.tickets.actions.delete_ticket(), style: 4 } ] @@ -52,9 +56,9 @@ const reportTicketClosedMessage: LoadedMessageTemplate = { ] } ] -}; +}); -export default reportTicketClosedMessage; +export default ticketClosedReportMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-closed.ts b/messages/tickets/ticket-closed.ts index b88fa80e..5dda4ba7 100644 --- a/messages/tickets/ticket-closed.ts +++ b/messages/tickets/ticket-closed.ts @@ -14,9 +14,9 @@ This notice must not be removed, obscured, or replaced. */ import { ComponentType } from "@discordjs/core"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketClosedMessage: LoadedMessageTemplate = { +const ticketClosedMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.Container, @@ -24,19 +24,23 @@ const ticketClosedMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, - content: "## Ticket Closed" + content: LL.tickets.templates.ticket_closed.title() }, { type: ComponentType.TextDisplay, - content: "<@{userId}>'s ticket has been closed." + content: LL.tickets.templates.ticket_closed.subtitle({ userId: "{userId}" }) }, { type: ComponentType.TextDisplay, - content: "**Reason**: {reason}\n**Claim**: {claimStatus}\n**Transcript**: {transcriptStatus}" + content: LL.tickets.templates.ticket_closed.details({ + reason: "{reason}", + claimStatus: "{claimStatus}", + transcriptStatus: "{transcriptStatus}" + }) }, { type: ComponentType.TextDisplay, - content: "-# _Closed by {closerName}_" + content: LL.tickets.templates.ticket_closed.closed_by({ closerName: "{closerName}" }) }, { type: ComponentType.ActionRow, @@ -44,7 +48,7 @@ const ticketClosedMessage: LoadedMessageTemplate = { { type: ComponentType.Button, custom_id: "{deleteButtonCustomId}", - label: "Delete Ticket", + label: LL.tickets.actions.delete_ticket(), style: 4 } ] @@ -52,7 +56,7 @@ const ticketClosedMessage: LoadedMessageTemplate = { ] } ] -}; +}); export default ticketClosedMessage; diff --git a/messages/tickets/ticket-opened-billing.ts b/messages/tickets/ticket-opened-billing.ts index 97d358fc..563f0447 100644 --- a/messages/tickets/ticket-opened-billing.ts +++ b/messages/tickets/ticket-opened-billing.ts @@ -15,9 +15,9 @@ This notice must not be removed, obscured, or replaced. import { ComponentType } from "@discordjs/core"; import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const billingTicketOpenedMessage: LoadedMessageTemplate = { +const ticketOpenedBillingMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.TextDisplay, @@ -25,32 +25,32 @@ const billingTicketOpenedMessage: LoadedMessageTemplate = { }, { type: ComponentType.Container, - accent_color: 15844367, + accent_color: 16106539, components: [ { type: ComponentType.TextDisplay, - content: "## Billing Ticket" + content: LL.tickets.templates.ticket_opened_billing.title() }, { type: ComponentType.TextDisplay, - content: "Include invoice numbers, payment method, and any failed transaction details." + content: LL.tickets.templates.ticket_opened_billing.intro() }, { type: ComponentType.TextDisplay, - content: "**Submitted Details**\n{reason}" + content: LL.tickets.templates.ticket_opened_billing.details_label({ reason: "{reason}" }) }, createRuntimeTextSlot(), { type: ComponentType.TextDisplay, - content: "**Claim Status**: {claimStatus}" + content: LL.tickets.templates.ticket_opened_billing.claim_status({ claimStatus: "{claimStatus}" }) }, createMessageSlot("actions") ] } ] -}; +}); -export default billingTicketOpenedMessage; +export default ticketOpenedBillingMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-opened-general.ts b/messages/tickets/ticket-opened-general.ts index 5fd4b735..7f873781 100644 --- a/messages/tickets/ticket-opened-general.ts +++ b/messages/tickets/ticket-opened-general.ts @@ -15,9 +15,9 @@ This notice must not be removed, obscured, or replaced. import { ComponentType } from "@discordjs/core"; import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const generalTicketOpenedMessage: LoadedMessageTemplate = { +const ticketOpenedGeneralMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.TextDisplay, @@ -25,32 +25,32 @@ const generalTicketOpenedMessage: LoadedMessageTemplate = { }, { type: ComponentType.Container, - accent_color: 3447003, + accent_color: 16106539, components: [ { type: ComponentType.TextDisplay, - content: "## General Support Ticket" + content: LL.tickets.templates.ticket_opened_general.title() }, { type: ComponentType.TextDisplay, - content: "A support team member will review this request soon." + content: LL.tickets.templates.ticket_opened_general.intro() }, { type: ComponentType.TextDisplay, - content: "**Summary**\n{reason}" + content: LL.tickets.templates.ticket_opened_general.details_label({ reason: "{reason}" }) }, createRuntimeTextSlot(), { type: ComponentType.TextDisplay, - content: "**Claim Status**: {claimStatus}" + content: LL.tickets.templates.ticket_opened_general.claim_status({ claimStatus: "{claimStatus}" }) }, createMessageSlot("actions") ] } ] -}; +}); -export default generalTicketOpenedMessage; +export default ticketOpenedGeneralMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-opened-report.ts b/messages/tickets/ticket-opened-report.ts index b3c01686..62932386 100644 --- a/messages/tickets/ticket-opened-report.ts +++ b/messages/tickets/ticket-opened-report.ts @@ -15,9 +15,9 @@ This notice must not be removed, obscured, or replaced. import { ComponentType } from "@discordjs/core"; import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const reportTicketOpenedMessage: LoadedMessageTemplate = { +const ticketOpenedReportMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.TextDisplay, @@ -25,32 +25,32 @@ const reportTicketOpenedMessage: LoadedMessageTemplate = { }, { type: ComponentType.Container, - accent_color: 15158332, + accent_color: 16106539, components: [ { type: ComponentType.TextDisplay, - content: "## Report Ticket" + content: LL.tickets.templates.ticket_opened_report.title() }, { type: ComponentType.TextDisplay, - content: "Moderation staff will review the report and any evidence attached." + content: LL.tickets.templates.ticket_opened_report.intro() }, { type: ComponentType.TextDisplay, - content: "**Report Details**\n{reason}" + content: LL.tickets.templates.ticket_opened_report.details_label({ reason: "{reason}" }) }, createRuntimeTextSlot(), { type: ComponentType.TextDisplay, - content: "**Claim Status**: {claimStatus}" + content: LL.tickets.templates.ticket_opened_report.claim_status({ claimStatus: "{claimStatus}" }) }, createMessageSlot("actions") ] } ] -}; +}); -export default reportTicketOpenedMessage; +export default ticketOpenedReportMessage; /* Ticket-Bot is licensed under the GNU Affero General Public License, diff --git a/messages/tickets/ticket-opened.ts b/messages/tickets/ticket-opened.ts index d1cf27e6..b9fb0d89 100644 --- a/messages/tickets/ticket-opened.ts +++ b/messages/tickets/ticket-opened.ts @@ -15,9 +15,9 @@ This notice must not be removed, obscured, or replaced. import { ComponentType } from "@discordjs/core"; import { createMessageSlot, createRuntimeTextSlot } from "@/features/tickets/messages"; -import type { LoadedMessageTemplate } from "@/features/tickets/types"; +import type { LoadedMessageTemplate, MessageTemplateContext } from "@/features/tickets/types"; -const ticketOpenedMessage: LoadedMessageTemplate = { +const ticketOpenedMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({ components: [ { type: ComponentType.TextDisplay, @@ -29,26 +29,26 @@ const ticketOpenedMessage: LoadedMessageTemplate = { components: [ { type: ComponentType.TextDisplay, - content: "## {ticketTypeName} Ticket" + content: LL.tickets.templates.ticket_opened.title({ ticketTypeName: "{ticketTypeName}" }) }, { type: ComponentType.TextDisplay, - content: "Thanks for opening a ticket." + content: LL.tickets.templates.ticket_opened.intro() }, { type: ComponentType.TextDisplay, - content: "**Details**\n{reason}" + content: LL.tickets.templates.ticket_opened.details_label({ reason: "{reason}" }) }, createRuntimeTextSlot(), { type: ComponentType.TextDisplay, - content: "**Claim Status**: {claimStatus}" + content: LL.tickets.templates.ticket_opened.claim_status({ claimStatus: "{claimStatus}" }) }, createMessageSlot("actions") ] } ] -}; +}); export default ticketOpenedMessage; From 88d23f99dbb9148b58cc0ff193db84e6ee8b3c4e Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:17:36 +0200 Subject: [PATCH 34/67] chore(dependencies): update package versions and schema in configuration files --- biome.json | 2 +- bun.lock | 68 +++++++++++++++++++++++++++++----------------------- package.json | 14 +++++------ 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/biome.json b/biome.json index c447b897..e7f7dba9 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.12/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, diff --git a/bun.lock b/bun.lock index b6a241f0..a5f927af 100644 --- a/bun.lock +++ b/bun.lock @@ -8,39 +8,39 @@ "@discordjs/rest": "^2.6.1", "@discordjs/ws": "^2.0.4", "@libsql/client": "^0.17.2", - "@ticketpm/core": "^0.0.6", - "@ticketpm/discord-api": "^0.0.6", - "discord-api-types": "^0.38.44", - "dotenv": "^17.4.1", + "@ticketpm/core": "^0.0.7", + "@ticketpm/discord-api": "^0.0.7", + "discord-api-types": "^0.38.47", + "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "typesafe-i18n": "^5.27.1", }, "devDependencies": { - "@biomejs/biome": "^2.4.10", - "@types/bun": "^1.3.11", - "@typescript/native-preview": "^7.0.0-dev.20260409.1", + "@biomejs/biome": "^2.4.12", + "@types/bun": "^1.3.12", + "@typescript/native-preview": "^7.0.0-dev.20260419.1", "drizzle-kit": "^0.31.10", }, }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.10", "@biomejs/cli-darwin-x64": "2.4.10", "@biomejs/cli-linux-arm64": "2.4.10", "@biomejs/cli-linux-arm64-musl": "2.4.10", "@biomejs/cli-linux-x64": "2.4.10", "@biomejs/cli-linux-x64-musl": "2.4.10", "@biomejs/cli-win32-arm64": "2.4.10", "@biomejs/cli-win32-x64": "2.4.10" }, "bin": { "biome": "bin/biome" } }, "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w=="], + "@biomejs/biome": ["@biomejs/biome@2.4.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.12", "@biomejs/cli-darwin-x64": "2.4.12", "@biomejs/cli-linux-arm64": "2.4.12", "@biomejs/cli-linux-arm64-musl": "2.4.12", "@biomejs/cli-linux-x64": "2.4.12", "@biomejs/cli-linux-x64-musl": "2.4.12", "@biomejs/cli-win32-arm64": "2.4.12", "@biomejs/cli-win32-x64": "2.4.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.10", "", { "os": "win32", "cpu": "x64" }, "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="], "@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], @@ -142,37 +142,37 @@ "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], - "@ticketpm/core": ["@ticketpm/core@0.0.6", "", {}, "sha512-wjzubX8a3tNePJeAGAlIr7vTBVNzlEOmD4CBC+tqwQhU1PiqQUJWeLKhkmh87a2NAd6AIRJXifUfzYRiHiZuIw=="], + "@ticketpm/core": ["@ticketpm/core@0.0.7", "", {}, "sha512-Zda8J8Vophs7R86pG57p1bCCkZlox/cGORRpg4OqnfFy/JnjHnYf/n9hKgTbb05AF0d0zTV8qdjMPh6ABDd2Wg=="], - "@ticketpm/discord-api": ["@ticketpm/discord-api@0.0.6", "", { "dependencies": { "@ticketpm/core": "0.0.6", "discord-api-types": "^0.37.119" } }, "sha512-1pKjIvy1Xzn4kLzfgfxrrSrpiLoa6YQpG5CEzaGQfWjuov7CK2OxFfcGZzKtMgBX2hNVsrQYycBqzzxJ4SoDzA=="], + "@ticketpm/discord-api": ["@ticketpm/discord-api@0.0.7", "", { "dependencies": { "@ticketpm/core": "0.0.7", "discord-api-types": "^0.37.119" } }, "sha512-FP2lbMik2+l8e+j6e22AxyHULoeXUkecud2o73ajqIUBvdunlUZblx3QwaAoLWLfAKJdrPjP4cTYcuJIzYXXWQ=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260409.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260409.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260409.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-CV1HEMGo1xCySwUJbCQOF+mmrTue8KTJ1Od2kKWhcbOpu8fPBfaqIpbAM6tGLcNEykEjMMTYHc/VTLbMgxdScQ=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260419.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260419.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260419.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260419.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260419.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260419.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260419.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260419.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-PzN1nqNe0B4vqyUzZD0Da9LbbdOYhcjLVS0Rkd/lDZEuLuovZCdIpXg1hz2S7JnD4P5P+Uy1aAdO5ubv8vQVTw=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260409.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GcRRnaoeZVrbC47woQ/2t3vPoQcTSjsWPEAQGtwNSdw7Z9TKxG4ES22ghJIQXd3ncTRCMJ+XELnnuqxVutkJ9w=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260419.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qgoZvmBsEE928tqDg7l27qciCrCWRU5bXdiWUykkt4+er3OaNYKUt+UtJvNiuNxiPaVwnRMEjnVyFiO/KNo3gA=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260409.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-7s8DXAa0Xpu/8PEjYIc4I36Ju7eVpoz9k3E+3WQdOF8pIPWYohiOj+zi68m9XYQck+rnkjUFo26ThVKqVetoMA=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260419.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-3+bXF/Mi4u3jbtuPp0gO4bjk+uaDhQLWR/h7JRrmL6KgVJQ9uywEOorx+1SgbidH6a1neO/pUPAlCvJ7+9zUQA=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "arm" }, "sha512-fOa07JBUXQpEPq+024g346inYZ2xp63ELuoRq6J0jwDWQ/ftCCuvdQNMncwFhsm1qlMdKT3S68NrnSxX16hiaw=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260419.1", "", { "os": "linux", "cpu": "arm" }, "sha512-e+BjeErUxpHb8FAYfCCrM9MvAl7yL4eYpYEW3jAaPle/K2+6BJPnUpytgB1fbrMWDmbnTWhC4m9NgTPfNHznxA=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cGTzTUqRGlIDwdtkDy6qTrvrqpe27W4CdgnFn0FpxpiWnaIi3wqjlzQ1grtqrqainw/yuPy5hn/I86sQgN6nvA=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260419.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cxK8SJ0tLeAV67hKfPEkPyv6rDIeL8AyGFHJFMz+gz3mKNy6ZEPK+9B8fl/JFGPylOhspgm6+IqFepRJcw74eg=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "x64" }, "sha512-lQrbc/BJKBxQrR1ttBDU5sYY1Hb2moFQgHL20T6nbapNqGpK4pzy64p+NK39O93D4omiCSk04pkchBCVrMPSAg=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260419.1", "", { "os": "linux", "cpu": "x64" }, "sha512-6cf/o48dN5px6mrW8Hq7duGpimoCFAlpEsgbjYWex8Vjv2EH+S1QcWUbh3L0cAhygnxnb4f2gwovBGJqKgf6jg=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260409.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-kmCafMo1xZlYx+9WnfpeZJ2tnB/CcJdR8QPX7j9vqcpe51D7b7Intmr921dD48KGpVh5YgjQ1MEFE5mjGqGMaA=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260419.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yz7AxGsNf1cueTUv0SF5Q1LvRGVHh+F4cSwmwucGVkHJGKdLCQO7cLs4MTFY0gcqsLNr7qKH7gMh5WCQqsPVXQ=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260409.1", "", { "os": "win32", "cpu": "x64" }, "sha512-WRd+JpQipTsE15QgYr3w7J0f1NKvGcq2QEgmcq8hB0WZA1X2WhQopNu+MpPQ3tdDD42VjMhm8ZoB8HpuOoXK5w=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260419.1", "", { "os": "win32", "cpu": "x64" }, "sha512-3sXtVGB4dt/kNp/bQMj0H1q7WhFzVe//rTin9DWV9CpbPXY+Ue8aUIRNevneHBPp5zSsBkkJlg1BlMH1Z87mGA=="], "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], @@ -180,9 +180,9 @@ "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], - "discord-api-types": ["discord-api-types@0.38.44", "", {}, "sha512-q91MgBzP/gRaCLIbQTaOrOhbD8uVIaPKxpgX2sfFB2nZ9nSiTYM9P3NFQ7cbO6NCxctI6ODttc67MI+YhIfILg=="], + "discord-api-types": ["discord-api-types@0.38.47", "", {}, "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA=="], - "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], @@ -238,6 +238,14 @@ "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "@discordjs/core/discord-api-types": ["discord-api-types@0.38.44", "", {}, "sha512-q91MgBzP/gRaCLIbQTaOrOhbD8uVIaPKxpgX2sfFB2nZ9nSiTYM9P3NFQ7cbO6NCxctI6ODttc67MI+YhIfILg=="], + + "@discordjs/rest/discord-api-types": ["discord-api-types@0.38.44", "", {}, "sha512-q91MgBzP/gRaCLIbQTaOrOhbD8uVIaPKxpgX2sfFB2nZ9nSiTYM9P3NFQ7cbO6NCxctI6ODttc67MI+YhIfILg=="], + + "@discordjs/util/discord-api-types": ["discord-api-types@0.38.44", "", {}, "sha512-q91MgBzP/gRaCLIbQTaOrOhbD8uVIaPKxpgX2sfFB2nZ9nSiTYM9P3NFQ7cbO6NCxctI6ODttc67MI+YhIfILg=="], + + "@discordjs/ws/discord-api-types": ["discord-api-types@0.38.44", "", {}, "sha512-q91MgBzP/gRaCLIbQTaOrOhbD8uVIaPKxpgX2sfFB2nZ9nSiTYM9P3NFQ7cbO6NCxctI6ODttc67MI+YhIfILg=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@ticketpm/discord-api/discord-api-types": ["discord-api-types@0.37.120", "", {}, "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw=="], diff --git a/package.json b/package.json index d25af756..16b6d637 100644 --- a/package.json +++ b/package.json @@ -18,17 +18,17 @@ "@discordjs/rest": "^2.6.1", "@discordjs/ws": "^2.0.4", "@libsql/client": "^0.17.2", - "@ticketpm/core": "^0.0.6", - "@ticketpm/discord-api": "^0.0.6", - "discord-api-types": "^0.38.44", - "dotenv": "^17.4.1", + "@ticketpm/core": "^0.0.7", + "@ticketpm/discord-api": "^0.0.7", + "discord-api-types": "^0.38.47", + "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", "typesafe-i18n": "^5.27.1" }, "devDependencies": { - "@biomejs/biome": "^2.4.10", - "@types/bun": "^1.3.11", - "@typescript/native-preview": "^7.0.0-dev.20260409.1", + "@biomejs/biome": "^2.4.12", + "@types/bun": "^1.3.12", + "@typescript/native-preview": "^7.0.0-dev.20260419.1", "drizzle-kit": "^0.31.10" } } From 69a78e4921b9d508b1a8ffdc9ebd360302337689 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:20:45 +0200 Subject: [PATCH 35/67] fix(tickets): resolve type issue in action row map * Extract button disabling logic to disableTicketActionButton * Ensure strict typing for APIButtonComponentWithCustomId * Resolve TypeScript errors from anonymous structural types in discriminated unions --- src/features/tickets/close-workflow.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index 4df274f3..6dbc9bfd 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -421,11 +421,18 @@ function disableTicketActionRow( return { ...row, components: row.components.map((component) => - isTicketActionButton(component, disabledButtonIds) ? { ...component, disabled: true } : component + isTicketActionButton(component, disabledButtonIds) ? (disableTicketActionButton(component) as T) : component ) as T[] }; } +function disableTicketActionButton(button: APIButtonComponentWithCustomId): APIButtonComponentWithCustomId { + return { + ...button, + disabled: true + }; +} + function isTicketActionButton( component: APIComponentInMessageActionRow, disabledButtonIds: Set From 7089cdd32ad796a3e2d18ebea075fe38d40a68f0 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:29:35 +0200 Subject: [PATCH 36/67] feat(docker): add Dockerfile and docker-compose for containerization --- .dockerignore | 7 +++++++ Dockerfile | 17 +++++++++++++++++ docker-compose.yml | 15 +++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..abed0df5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.github +.vscode +node_modules +.data +config/.env +dist diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..aee1d96f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM oven/bun:1 + +WORKDIR /app + +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile +RUN bun run drizzle:push + +COPY . . + +# Keep the SQLite target directory available even before the bind mount exists. +RUN mkdir -p /app/.data + +ENV NODE_ENV=production + + +CMD ["bun", "run", "start"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3262d345 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +name: ticket-bot + +services: + bot: + container_name: ticket-bot + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + env_file: + - ./config/.env + volumes: + - ./config:/app/config:ro + - ./messages:/app/messages:ro + - ./.data:/app/.data From c4f0ae6fb9fe60a9b35dfe9b7cd644adb49f0ccd Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:11:41 +0200 Subject: [PATCH 37/67] refactor(tickets): allow unicode characters in channel names * Remove strict alphanumeric regex filter to support emojis and unicode * Add prefix/suffix whitespace trimming * Retain lowercase conversion and dash deduplication for API safety --- src/features/tickets/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/tickets/utils.ts b/src/features/tickets/utils.ts index 66b8c97d..6e3563e6 100644 --- a/src/features/tickets/utils.ts +++ b/src/features/tickets/utils.ts @@ -27,11 +27,11 @@ export function renderChannelName(template: string, tokens: Record Date: Sun, 19 Apr 2026 14:19:47 +0200 Subject: [PATCH 38/67] feat(readme): add README file --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..0ea14095 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Ticket-Bot + +Ticket Bot is a open-source Discord bot that allows you to easily manage support tickets on your server. It is built with `@discordjs/core` for a lower memory footprint than `discord.js`. + +![discord ticket bot typescript](https://i.imgur.com/qEzUBOL.png) + +## 📄 Documentation + +The documentation is available [here](https://doc.ticket.pm/) + +## 💬 Discord + +Ask questions and get support on our [Discord server](https://discord.gg/VasYV6MEJy). + +## ✨ Contributing + +Contributions are welcome! Please read the [contributing guidelines](https://github.com/Sayrix/Ticket-Bot/blob/main/CONTRIBUTING.md) first. + +## 👨‍💻 Maintainers +Our current project maintainers: +* [Sayrix](https://github.com/Sayrix) +* [小兽兽/zhiyan114](https://github.com/zhiyan114) + +## 💎 Sponsors +Thanks to all our sponsors! 🙏 +You can see all perks here: https://github.com/sponsors/Sayrix +

+ + + +

+ +## 🎥 Videos +- Have you made a video about Ticket-Bot? Please share it with us on our [Discord server](https://discord.gg/VasYV6MEJy) to get it listed here! + +## Please leave a ⭐ to help the project! + +## License +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. \ No newline at end of file From 3a75f78fc425dba2cf93beaf750469b4e579d844 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:46:20 +0200 Subject: [PATCH 39/67] feat(attribution): update bot status and ensure application attribution to comply with license terms --- config/config.example.ts | 4 ++-- src/events/ready.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/config/config.example.ts b/config/config.example.ts index df77bc33..2220c03d 100644 --- a/config/config.example.ts +++ b/config/config.example.ts @@ -49,8 +49,8 @@ export default defineConfig("0.0.1", { status: { // Set to false to leave the bot presence untouched. enabled: true, - // Activity text shown in the member list. - text: "github.com/Sayrix", + // Activity text shown in the member list. Please include a link to the project in either the bot's status or profile description or the embeds to comply with the license terms. + text: "Powered by git.new/ticketbot", // PLAYING, STREAMING, LISTENING, WATCHING, CUSTOM, COMPETING type: "WATCHING", // Only used for STREAMING. diff --git a/src/events/ready.ts b/src/events/ready.ts index 5b895a24..2809e457 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -58,6 +58,7 @@ const readyEvent = defineEvent<[ToEventProps]>({ once: true, async execute(app, event) { await validateStartupEnvironment(app, event.data.user.id); + await ensureApplicationAttribution(app); app.logger.info(`Connected as ${event.data.user.username}.`); await deployApplicationCommands({ @@ -186,6 +187,33 @@ async function fetchSponsors() { } } +async function ensureApplicationAttribution(app: BotApp) { + try { + const application = await app.client.api.applications.getCurrent(); + const currentDescription = application.description ?? ""; + + // PLEASE RESPECT THE LICENSE: EITHER CREDIT THE PROJECT IN THE BOT'S DESCRIPTION OR STATUS OR EMBEDS, OR DON'T USE THE SOFTWARE AT ALL. THANK YOU. + if (currentDescription.includes("*Powered by https://git.new/ticketbot*")) { + // PLEASE RESPECT THE LICENSE: EITHER CREDIT THE PROJECT IN THE BOT'S DESCRIPTION OR STATUS OR EMBEDS, OR DON'T USE THE SOFTWARE AT ALL. THANK YOU. + return; + } + + const nextDescription = currentDescription.trimEnd() + ? // PLEASE RESPECT THE LICENSE: EITHER CREDIT THE PROJECT IN THE BOT'S DESCRIPTION OR STATUS OR EMBEDS, OR DON'T USE THE SOFTWARE AT ALL. THANK YOU. + `${currentDescription.trimEnd()}\n\n*Powered by https://git.new/ticketbot*` + : // PLEASE RESPECT THE LICENSE: EITHER CREDIT THE PROJECT IN THE BOT'S DESCRIPTION OR STATUS OR EMBEDS, OR DON'T USE THE SOFTWARE AT ALL. THANK YOU. + "*Powered by https://git.new/ticketbot*"; + // PLEASE RESPECT THE LICENSE: EITHER CREDIT THE PROJECT IN THE BOT'S DESCRIPTION OR STATUS OR EMBEDS, OR DON'T USE THE SOFTWARE AT ALL. THANK YOU. + + // PLEASE RESPECT THE LICENSE: EITHER CREDIT THE PROJECT IN THE BOT'S DESCRIPTION OR STATUS OR EMBEDS, OR DON'T USE THE SOFTWARE AT ALL. THANK YOU. + await app.client.api.applications.editCurrent({ + description: nextDescription + }); + } catch (error) { + app.logger.warn("Failed to ensure Ticket-Bot attribution in the application description.", error); + } +} + async function applyConfiguredPresence(app: BotApp) { const configuredStatus = app.config.status; From f3f0455ecf0811a2b4af258e308843eaeacbfa6f Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:39:54 +0200 Subject: [PATCH 40/67] chore(github): update code owners and fix typos * Add new code owners for /messages/ and /i18n/ * Fix casing on Dockerfile and docker-compose.yml --- .github/CODEOWNERS | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d6a4c696..a4b516d3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,14 +1,14 @@ # Main Source /src/ @sayrix @zhiyan114 /config/ @sayrix @zhiyan114 +/messages/ @sayrix @zhiyan114 # Docker files -dockerfile @zhiyan114 -/docker_run.sh @zhiyan114 -/docker-compose.yml @zhiyan114 - -# Database schema -/prisma/* @zhiyan114 +Dockerfile @sayrix +/docker-compose.yml @sayrix # Langs -/locales/ @sayrix +/i18n/ @sayrix + +# All other files +* @sayrix \ No newline at end of file From 90a0abb80980a060b63001e46a041480be738bbe Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:40:14 +0200 Subject: [PATCH 41/67] build(core): migrate tooling to cross-runtime environment * Add scripts for compilation and file alias rewriting * Switch module types and imports to Node ESM defaults * Introduce dist folder to Biome ignore lists * Replace Bun-specific types with Node type definitions * Introduce new deployment CLI entrypoint script --- biome.json | 6 +- bun.lock | 127 +++++++++++++++++++------------------ package.json | 28 ++++++--- scripts/build.mjs | 143 ++++++++++++++++++++++++++++++++++++++++++ scripts/run-entry.mjs | 42 +++++++++++++ tsconfig.build.json | 14 +++++ tsconfig.json | 6 +- 7 files changed, 290 insertions(+), 76 deletions(-) create mode 100644 scripts/build.mjs create mode 100644 scripts/run-entry.mjs create mode 100644 tsconfig.build.json diff --git a/biome.json b/biome.json index e7f7dba9..5bd60aea 100644 --- a/biome.json +++ b/biome.json @@ -20,14 +20,14 @@ } } }, - "includes": ["**", "!**/node_modules/", "!**/bun.lock"] + "includes": ["**", "!**/node_modules/", "!**/dist/", "!**/bun.lock"] }, "formatter": { "enabled": true, "indentStyle": "tab", "indentWidth": 2, "lineWidth": 130, - "includes": ["**", "!**/node_modules/", "!**/bun.lock"] + "includes": ["**", "!**/node_modules/", "!**/dist/", "!**/bun.lock"] }, "html": { "formatter": { @@ -49,6 +49,6 @@ } }, "files": { - "includes": ["**", "!**/node_modules/", "!**/bun.lock"] + "includes": ["**", "!**/node_modules/", "!**/dist/", "!**/bun.lock"] } } diff --git a/bun.lock b/bun.lock index a5f927af..33d46afe 100644 --- a/bun.lock +++ b/bun.lock @@ -12,14 +12,15 @@ "@ticketpm/discord-api": "^0.0.7", "discord-api-types": "^0.38.47", "dotenv": "^17.4.2", + "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", "typesafe-i18n": "^5.27.1", }, "devDependencies": { "@biomejs/biome": "^2.4.12", - "@types/bun": "^1.3.12", + "@types/node": "^24.9.1", "@typescript/native-preview": "^7.0.0-dev.20260419.1", - "drizzle-kit": "^0.31.10", + "tsx": "^4.21.0", }, }, }, @@ -58,57 +59,57 @@ "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], "@libsql/client": ["@libsql/client@0.17.2", "", { "dependencies": { "@libsql/core": "^0.17.2", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q=="], @@ -146,9 +147,7 @@ "@ticketpm/discord-api": ["@ticketpm/discord-api@0.0.7", "", { "dependencies": { "@ticketpm/core": "0.0.7", "discord-api-types": "^0.37.119" } }, "sha512-FP2lbMik2+l8e+j6e22AxyHULoeXUkecud2o73ajqIUBvdunlUZblx3QwaAoLWLfAKJdrPjP4cTYcuJIzYXXWQ=="], - "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], - - "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -188,7 +187,7 @@ "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], @@ -228,7 +227,7 @@ "undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], @@ -250,9 +249,13 @@ "@ticketpm/discord-api/discord-api-types": ["discord-api-types@0.37.120", "", {}, "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw=="], + "@types/ws/@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + + "bun-types/@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], @@ -298,56 +301,60 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + "@types/ws/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], } } diff --git a/package.json b/package.json index 16b6d637..01649cca 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,22 @@ "version": "4.0.0", "license": "AGPL-3.0-only", "description": "Open-source Discord ticket bot.", - "main": "src/index.ts", + "type": "module", + "main": "./dist/src/index.js", + "engines": { + "node": ">=20.0.0", + "bun": ">=1.0.0" + }, "scripts": { - "typecheck": "tsgo --noEmit", - "i18n": "bunx typesafe-i18n --no-watch", - "lint": "biome lint", - "format": "biome format", + "typecheck": "tsc --noEmit -p tsconfig.json", + "i18n": "typesafe-i18n --no-watch", + "lint": "biome lint .", + "format": "biome format .", "format:fix": "biome format --write", - "drizzle:push": "bunx drizzle-kit push", - "start": "bun src/index.ts" + "drizzle:push": "drizzle-kit push", + "build": "bun scripts/build.mjs || node scripts/build.mjs", + "deploy:commands": "bun scripts/run-entry.mjs src/deploy-commands-cli.js || node scripts/run-entry.mjs src/deploy-commands-cli.js", + "start": "bun scripts/run-entry.mjs src/index.js || node scripts/run-entry.mjs src/index.js" }, "dependencies": { "@discordjs/core": "2.4.0", @@ -23,12 +30,13 @@ "discord-api-types": "^0.38.47", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", + "drizzle-kit": "^0.31.10", "typesafe-i18n": "^5.27.1" }, "devDependencies": { "@biomejs/biome": "^2.4.12", - "@types/bun": "^1.3.12", - "@typescript/native-preview": "^7.0.0-dev.20260419.1", - "drizzle-kit": "^0.31.10" + "@types/node": "^24.9.1", + "typescript": "^6.0.2", + "tsx": "^4.21.0" } } diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 00000000..acabc743 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,143 @@ +import { spawn } from "node:child_process"; +import { readdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; +import ts from "typescript"; + +const rootDirectory = fileURLToPath(new URL("..", import.meta.url)); +const distDirectory = path.join(rootDirectory, "dist"); +const tsconfigPath = path.join(rootDirectory, "tsconfig.build.json"); +const tscEntrypoint = path.join(rootDirectory, "node_modules", "typescript", "bin", "tsc"); + +await rm(distDirectory, { recursive: true, force: true }); +await run(process.execPath, [tscEntrypoint, "--project", tsconfigPath]); +await rewriteDirectory(distDirectory); + +async function rewriteDirectory(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + await rewriteDirectory(entryPath); + continue; + } + + if (!entry.isFile() || path.extname(entry.name) !== ".js") { + continue; + } + + const source = await readFile(entryPath, "utf8"); + const rewritten = rewriteImports(source, entryPath); + + if (rewritten !== source) { + await writeFile(entryPath, rewritten); + } + } +} + +function rewriteImports(source, filePath) { + const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); + const edits = []; + + const visit = (node) => { + if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) { + edits.push({ + start: node.moduleSpecifier.getStart(sourceFile) + 1, + end: node.moduleSpecifier.getEnd() - 1, + value: rewriteSpecifier(node.moduleSpecifier.text, filePath) + }); + } + + if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { + const [argument] = node.arguments; + + if (argument && ts.isStringLiteralLike(argument)) { + edits.push({ + start: argument.getStart(sourceFile) + 1, + end: argument.getEnd() - 1, + value: rewriteSpecifier(argument.text, filePath) + }); + } + } + + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + + return edits + .sort((left, right) => right.start - left.start) + .reduce((currentSource, edit) => { + return `${currentSource.slice(0, edit.start)}${edit.value}${currentSource.slice(edit.end)}`; + }, source); +} + +function rewriteSpecifier(specifier, filePath) { + if (specifier.startsWith("@/")) { + const targetPath = path.join(rootDirectory, "dist", "src", specifier.slice(2)); + return toImportPath(path.relative(path.dirname(filePath), ensureJsExtension(targetPath))); + } + + if (specifier.startsWith(".")) { + return normalizeRelativeExtension(specifier); + } + + return specifier; +} + +function normalizeRelativeExtension(specifier) { + if (/\.(?:c|m)?js$|\.json$|\.node$/u.test(specifier)) { + return specifier; + } + + if (/\.(?:cts|mts|tsx|ts)$/u.test(specifier)) { + return specifier.replace(/\.(?:cts|mts|tsx|ts)$/u, ".js"); + } + + return `${specifier}.js`; +} + +function ensureJsExtension(filePath) { + if (/\.(?:c|m)?js$/u.test(filePath)) { + return filePath; + } + + if (/\.(?:cts|mts|tsx|ts)$/u.test(filePath)) { + return filePath.replace(/\.(?:cts|mts|tsx|ts)$/u, ".js"); + } + + return `${filePath}.js`; +} + +function toImportPath(relativePath) { + const normalizedPath = relativePath.split(path.sep).join("/"); + + if (normalizedPath.startsWith(".")) { + return normalizedPath; + } + + return `./${normalizedPath}`; +} + +function run(command, args) { + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn(command, args, { + cwd: rootDirectory, + env: process.env, + stdio: "inherit" + }); + + child.on("error", rejectPromise); + child.on("exit", (code) => { + if (code === 0) { + resolvePromise(); + return; + } + + rejectPromise(new Error(`Command failed with exit code ${code ?? "unknown"}.`)); + }); + }); +} diff --git a/scripts/run-entry.mjs b/scripts/run-entry.mjs new file mode 100644 index 00000000..c53ffdb1 --- /dev/null +++ b/scripts/run-entry.mjs @@ -0,0 +1,42 @@ +import { spawn } from "node:child_process"; +import { stat } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const rootDirectory = fileURLToPath(new URL("..", import.meta.url)); +const entryArgument = process.argv[2]; + +if (!entryArgument) { + throw new Error("Missing dist entry argument."); +} + +const distEntry = path.join(rootDirectory, "dist", entryArgument); + +try { + await stat(distEntry); +} catch { + await run(process.execPath, [path.join(rootDirectory, "scripts", "build.mjs")]); +} + +await run(process.execPath, [distEntry]); + +function run(command, args) { + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn(command, args, { + cwd: rootDirectory, + env: process.env, + stdio: "inherit" + }); + + child.on("error", rejectPromise); + child.on("exit", (code) => { + if (code === 0) { + resolvePromise(); + return; + } + + rejectPromise(new Error(`Command failed with exit code ${code ?? "unknown"}.`)); + }); + }); +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..fdad14fd --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": false, + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": false, + "outDir": "./dist", + "rootDir": ".", + "sourceMap": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "config/**/*.ts", "i18n/**/*.ts", "messages/**/*.ts", "drizzle.config.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/tsconfig.json b/tsconfig.json index f2267e8a..bb3d00b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,6 @@ // Bundler mode "moduleResolution": "bundler", - "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, "ignoreDeprecations": "6.0", @@ -25,10 +24,11 @@ "noPropertyAccessFromIndexSignature": false, // Type definitions - "types": ["bun"], + "types": ["node"], "paths": { "@/*": ["./src/*"] } - } + }, + "exclude": ["dist", "node_modules"] } From 8360b299659aa12212f2e40ca771bdb3be54b4e8 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:48:27 +0200 Subject: [PATCH 42/67] refactor(core): adapt code and internals for multi-runtime support * Create explicit CLI entrypoint for deploying commands * Modify ES modules imports to use .js extensions explicitly * Remove polyfills and process.versions.bun rigid checks inside telemetry * Update message template imports handling in Node environment --- config/config.example.ts | 2 +- src/app.ts | 2 +- src/deploy-commands-cli.ts | 36 ++++++++++++++++++++++++++++++++ src/deploy-commands.ts | 11 ++-------- src/features/tickets/messages.ts | 11 +++++----- src/telemetry.ts | 6 ++++-- 6 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 src/deploy-commands-cli.ts diff --git a/config/config.example.ts b/config/config.example.ts index 2220c03d..bb1c2c6f 100644 --- a/config/config.example.ts +++ b/config/config.example.ts @@ -13,7 +13,7 @@ project repository or to its website. This notice must not be removed, obscured, or replaced. */ -import { defineConfig } from "@/config/index.ts"; +import { defineConfig } from "@/config/index.js"; export default defineConfig("0.0.1", { // Your Discord application (bot) client ID. diff --git a/src/app.ts b/src/app.ts index 35b9d59b..d79e916b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -23,7 +23,7 @@ import { createLogger } from "@/core/logger"; import { createHandlerRegistry, registerEvents } from "@/core/registry"; import { InteractionRouter } from "@/core/router"; import type { BotApp } from "@/core/types"; -import botConfig from "../config/config.ts"; +import botConfig from "../config/config.js"; export async function createBotApp() { const logger = createLogger("bot"); diff --git a/src/deploy-commands-cli.ts b/src/deploy-commands-cli.ts new file mode 100644 index 00000000..32fdf92a --- /dev/null +++ b/src/deploy-commands-cli.ts @@ -0,0 +1,36 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { deployCommands } from "@/deploy-commands"; + +deployCommands().catch((error) => { + console.error("[deploy] Failed to deploy commands", error); + process.exit(1); +}); + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index 755fc2d1..f5d87c90 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -21,7 +21,7 @@ import { discoverCommands, discoverEvents, discoverFeatures } from "@/core/disco import { createBotI18n } from "@/core/i18n"; import { createLogger, type Logger } from "@/core/logger"; import { createHandlerRegistry } from "@/core/registry"; -import botConfig from "../config/config.ts"; +import botConfig from "../config/config.js"; config({ path: "./config/.env", quiet: true }); const logger = createLogger("deploy"); @@ -48,7 +48,7 @@ export async function deployApplicationCommands(options: { options.logger.info(`Deployed ${options.applicationCommands.length} global commands.`); } -async function deployCommands() { +export async function deployCommands() { const [commands, events, features] = await Promise.all([ discoverCommands(logger), discoverEvents(logger), @@ -66,13 +66,6 @@ async function deployCommands() { }); } -if (import.meta.main) { - deployCommands().catch((error) => { - logger.error("Failed to deploy commands", error); - process.exit(1); - }); -} - /* Ticket-Bot is licensed under the GNU Affero General Public License, version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. diff --git a/src/features/tickets/messages.ts b/src/features/tickets/messages.ts index fd10d78a..9947c8c8 100644 --- a/src/features/tickets/messages.ts +++ b/src/features/tickets/messages.ts @@ -224,7 +224,8 @@ async function resolveMessageTemplatePath(reference: string) { throw new Error(`Message template reference "${reference}" resolves outside the messages directory.`); } - const candidatePaths = extname(resolvedBasePath) === "" ? [`${resolvedBasePath}.ts`] : [resolvedBasePath]; + const candidatePaths = + extname(resolvedBasePath) === "" ? [`${resolvedBasePath}.ts`, `${resolvedBasePath}.js`] : [resolvedBasePath]; for (const candidatePath of candidatePaths) { try { @@ -239,14 +240,14 @@ async function resolveMessageTemplatePath(reference: string) { async function loadMessageTemplateSource(filePath: string) { const extension = extname(filePath).toLowerCase(); - if (extension === ".ts") { - // Templates stay code-only in v4 so they remain typed and can opt into - // Components V2 without extra parsing layers. + if (extension === ".ts" || extension === ".js") { + // Templates stay code-only in v4 so they remain typed in source and still + // load correctly from the compiled JavaScript output. const importedModule = await import(pathToFileURL(filePath).href); return (importedModule.default ?? importedModule.message ?? importedModule) as MessageTemplateSource; } - throw new Error(`Unsupported template file type "${extension}". Only TypeScript templates are supported.`); + throw new Error(`Unsupported template file type "${extension}". Only JavaScript and TypeScript templates are supported.`); } function normalizeMessageTemplate(rawPayload: unknown): LoadedMessageTemplate { diff --git a/src/telemetry.ts b/src/telemetry.ts index 8f7efb76..58168c19 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -249,10 +249,12 @@ function shouldShowWSLog(app: BotApp) { } function getRuntimeInfo() { - if (typeof process.versions.bun === "string") { + const bunVersion = (process.versions as Record).bun; + + if (typeof bunVersion === "string") { return { name: "bun", - version: process.versions.bun + version: bunVersion } as const; } From 8cd468fd04edcda48c7d96e63b07234606e015a7 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:43:56 +0200 Subject: [PATCH 43/67] feat(readme): use GitHub wikis instead of Docusaurus for the v4 docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ea14095..4ff500ef 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Ticket Bot is a open-source Discord bot that allows you to easily manage support ## 📄 Documentation -The documentation is available [here](https://doc.ticket.pm/) +The documentation is available [here](https://github.com/Sayrix/Ticket-Bot/wiki) ## 💬 Discord From 762f7cd64b8a6056452095678569b0219088c1f5 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:20:21 +0200 Subject: [PATCH 44/67] feat(migration): add v3 config and database migration scripts - Implemented `migrate-v3-config.ts` to handle migration of v3 JSONC configuration to v4 TypeScript format. - Added `migrate-v3-db.ts` for migrating v3 SQLite database to v4 schema, including ticket and panel message handling. - Introduced command-line interface for both migration scripts with options for source, target, panel key, type mapping, and overwrite functionality. - Included error handling and validation for migration processes to ensure data integrity and user guidance. --- bun.lock | 21 +- package.json | 6 +- scripts/run-entry.mjs | 54 ++- scripts/verify-migrate-v3.mjs | 369 +++++++++++++++++++ src/migrate-v3-config.ts | 555 ++++++++++++++++++++++++++++ src/migrate-v3-db.ts | 657 ++++++++++++++++++++++++++++++++++ 6 files changed, 1639 insertions(+), 23 deletions(-) create mode 100644 scripts/verify-migrate-v3.mjs create mode 100644 src/migrate-v3-config.ts create mode 100644 src/migrate-v3-db.ts diff --git a/bun.lock b/bun.lock index 33d46afe..eaef5889 100644 --- a/bun.lock +++ b/bun.lock @@ -14,13 +14,14 @@ "dotenv": "^17.4.2", "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", + "jsonc-parser": "^3.3.1", "typesafe-i18n": "^5.27.1", }, "devDependencies": { "@biomejs/biome": "^2.4.12", "@types/node": "^24.9.1", - "@typescript/native-preview": "^7.0.0-dev.20260419.1", "tsx": "^4.21.0", + "typescript": "^6.0.2", }, }, }, @@ -151,22 +152,6 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260419.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260419.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260419.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260419.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260419.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260419.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260419.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260419.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-PzN1nqNe0B4vqyUzZD0Da9LbbdOYhcjLVS0Rkd/lDZEuLuovZCdIpXg1hz2S7JnD4P5P+Uy1aAdO5ubv8vQVTw=="], - - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260419.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qgoZvmBsEE928tqDg7l27qciCrCWRU5bXdiWUykkt4+er3OaNYKUt+UtJvNiuNxiPaVwnRMEjnVyFiO/KNo3gA=="], - - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260419.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-3+bXF/Mi4u3jbtuPp0gO4bjk+uaDhQLWR/h7JRrmL6KgVJQ9uywEOorx+1SgbidH6a1neO/pUPAlCvJ7+9zUQA=="], - - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260419.1", "", { "os": "linux", "cpu": "arm" }, "sha512-e+BjeErUxpHb8FAYfCCrM9MvAl7yL4eYpYEW3jAaPle/K2+6BJPnUpytgB1fbrMWDmbnTWhC4m9NgTPfNHznxA=="], - - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260419.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cxK8SJ0tLeAV67hKfPEkPyv6rDIeL8AyGFHJFMz+gz3mKNy6ZEPK+9B8fl/JFGPylOhspgm6+IqFepRJcw74eg=="], - - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260419.1", "", { "os": "linux", "cpu": "x64" }, "sha512-6cf/o48dN5px6mrW8Hq7duGpimoCFAlpEsgbjYWex8Vjv2EH+S1QcWUbh3L0cAhygnxnb4f2gwovBGJqKgf6jg=="], - - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260419.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yz7AxGsNf1cueTUv0SF5Q1LvRGVHh+F4cSwmwucGVkHJGKdLCQO7cLs4MTFY0gcqsLNr7qKH7gMh5WCQqsPVXQ=="], - - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260419.1", "", { "os": "win32", "cpu": "x64" }, "sha512-3sXtVGB4dt/kNp/bQMj0H1q7WhFzVe//rTin9DWV9CpbPXY+Ue8aUIRNevneHBPp5zSsBkkJlg1BlMH1Z87mGA=="], - "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -199,6 +184,8 @@ "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], diff --git a/package.json b/package.json index 01649cca..791082e3 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "drizzle:push": "drizzle-kit push", "build": "bun scripts/build.mjs || node scripts/build.mjs", "deploy:commands": "bun scripts/run-entry.mjs src/deploy-commands-cli.js || node scripts/run-entry.mjs src/deploy-commands-cli.js", + "migrate:v3": "bun scripts/run-entry.mjs src/migrate-v3-db.js", + "migrate:v3:config": "bun scripts/run-entry.mjs src/migrate-v3-config.js", + "migrate:v3:test": "bun scripts/verify-migrate-v3.mjs", "start": "bun scripts/run-entry.mjs src/index.js || node scripts/run-entry.mjs src/index.js" }, "dependencies": { @@ -29,8 +32,9 @@ "@ticketpm/discord-api": "^0.0.7", "discord-api-types": "^0.38.47", "dotenv": "^17.4.2", - "drizzle-orm": "^0.45.2", "drizzle-kit": "^0.31.10", + "drizzle-orm": "^0.45.2", + "jsonc-parser": "^3.3.1", "typesafe-i18n": "^5.27.1" }, "devDependencies": { diff --git a/scripts/run-entry.mjs b/scripts/run-entry.mjs index c53ffdb1..f657d7b9 100644 --- a/scripts/run-entry.mjs +++ b/scripts/run-entry.mjs @@ -1,11 +1,12 @@ import { spawn } from "node:child_process"; -import { stat } from "node:fs/promises"; +import { readdir, stat } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; const rootDirectory = fileURLToPath(new URL("..", import.meta.url)); const entryArgument = process.argv[2]; +const entryArgs = process.argv.slice(3); if (!entryArgument) { throw new Error("Missing dist entry argument."); @@ -13,13 +14,56 @@ if (!entryArgument) { const distEntry = path.join(rootDirectory, "dist", entryArgument); -try { - await stat(distEntry); -} catch { +if (await shouldBuild(distEntry)) { await run(process.execPath, [path.join(rootDirectory, "scripts", "build.mjs")]); } -await run(process.execPath, [distEntry]); +await run(process.execPath, [distEntry, ...entryArgs]); + +async function shouldBuild(distEntryPath) { + let distEntryStat; + + try { + distEntryStat = await stat(distEntryPath); + } catch { + return true; + } + + const latestSourceMtime = await getLatestSourceMtime([ + path.join(rootDirectory, "src"), + path.join(rootDirectory, "config"), + path.join(rootDirectory, "i18n"), + path.join(rootDirectory, "messages"), + path.join(rootDirectory, "drizzle.config.ts") + ]); + + return latestSourceMtime > distEntryStat.mtimeMs; +} + +async function getLatestSourceMtime(paths) { + let latest = 0; + + for (const sourcePath of paths) { + const sourceStat = await stat(sourcePath).catch(() => null); + + if (!sourceStat) { + continue; + } + + if (sourceStat.isDirectory()) { + const entries = await readdir(sourcePath, { withFileTypes: true }); + const childPaths = entries.map((entry) => path.join(sourcePath, entry.name)); + latest = Math.max(latest, await getLatestSourceMtime(childPaths)); + continue; + } + + if (sourceStat.isFile() && /\.(?:ts|mts|cts|js|mjs|json)$/u.test(sourcePath)) { + latest = Math.max(latest, sourceStat.mtimeMs); + } + } + + return latest; +} function run(command, args) { return new Promise((resolvePromise, rejectPromise) => { diff --git a/scripts/verify-migrate-v3.mjs b/scripts/verify-migrate-v3.mjs new file mode 100644 index 00000000..d5953879 --- /dev/null +++ b/scripts/verify-migrate-v3.mjs @@ -0,0 +1,369 @@ +import { spawn } from "node:child_process"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL, fileURLToPath } from "node:url"; +import { createClient } from "@libsql/client/sqlite3"; + +const rootDirectory = fileURLToPath(new URL("..", import.meta.url)); +const fixtureDirectory = path.join(rootDirectory, ".data"); + +await mkdir(fixtureDirectory, { recursive: true }); +await run(process.execPath, [path.join(rootDirectory, "scripts", "build.mjs")]); + +const configModule = await import(pathToFileURL(path.join(rootDirectory, "dist", "config", "config.js")).href); +const config = configModule.default; +const validTypeKey = Object.keys(config.ticketTypes)[0]; +const firstPanelKey = Object.keys(config.panels)[0]; + +if (!validTypeKey) { + throw new Error("config/config.ts must define at least one ticket type for migration verification."); +} + +await verifyBasicMigration(); +await verifyTypeMap(); +await verifyUnknownTypeFailure(); +await verifyTargetConflictFailure(); +await verifyConfigMigration(); + +console.log("[migrate:v3:test] All migration fixture checks passed."); + +async function verifyBasicMigration() { + const sourceUrl = await resetFixture("migrate-v3-basic-source.db"); + const targetUrl = await resetFixture("migrate-v3-basic-target.db"); + + await createV3Database(sourceUrl, validTypeKey, { includePanelMessage: Boolean(firstPanelKey) }); + await runMigration([ + "--source", + sourceUrl, + "--target", + targetUrl, + ...(firstPanelKey ? ["--panel-key", firstPanelKey] : []) + ]); + + await withClient(targetUrl, async (target) => { + const tickets = await target.execute("SELECT * FROM tickets ORDER BY id ASC"); + assert(tickets.rows.length === 2, "basic migration should copy two tickets."); + assert(Number(tickets.rows[0].id) === 10, "basic migration should preserve ticket IDs."); + assert(tickets.rows[0].type === validTypeKey, "basic migration should map category.codeName to type."); + assert(tickets.rows[0].claimedAt === null, "basic migration should preserve nullable claimedAt."); + assert(tickets.rows[1].claimedBy === "222222222222222222", "basic migration should copy claimedBy."); + assert(tickets.rows[1].closedReason === "Done", "basic migration should copy close reasons."); + assert(tickets.rows[1].transcriptUrl === "https://ticket.pm/transcript/abc", "basic migration should copy transcript URLs."); + assert(tickets.rows[0].invitedUserIds === JSON.stringify(["333333333333333333"]), "basic migration should copy invited users."); + + if (firstPanelKey) { + const panels = await target.execute("SELECT * FROM panel_messages"); + assert(panels.rows.length === 1, "basic migration should copy the open panel message."); + assert(panels.rows[0].panelKey === firstPanelKey, "basic migration should use the requested panel key."); + assert(panels.rows[0].messageId === "999999999999999999", "basic migration should copy openTicketMessageId."); + } + }); + + console.log("[migrate:v3:test] basic migration passed."); +} + +async function verifyTypeMap() { + const sourceUrl = await resetFixture("migrate-v3-type-map-source.db"); + const targetUrl = await resetFixture("migrate-v3-type-map-target.db"); + + await createV3Database(sourceUrl, "old-support", { includePanelMessage: false }); + await runMigration(["--source", sourceUrl, "--target", targetUrl, "--type-map", `old-support=${validTypeKey}`]); + + await withClient(targetUrl, async (target) => { + const tickets = await target.execute("SELECT type FROM tickets LIMIT 1"); + assert(tickets.rows[0]?.type === validTypeKey, "type map should rewrite old codeName values."); + }); + + console.log("[migrate:v3:test] type map migration passed."); +} + +async function verifyUnknownTypeFailure() { + const sourceUrl = await resetFixture("migrate-v3-unknown-source.db"); + const targetUrl = await resetFixture("migrate-v3-unknown-target.db"); + + await createV3Database(sourceUrl, "missing-type", { includePanelMessage: false }); + const result = await runMigration(["--source", sourceUrl, "--target", targetUrl], { expectFailure: true }); + + assertIncludes(result.stderr, 'Unknown v3 ticket type "missing-type"', "unknown type should fail with a useful error."); + console.log("[migrate:v3:test] unknown type failure passed."); +} + +async function verifyTargetConflictFailure() { + const sourceUrl = await resetFixture("migrate-v3-conflict-source.db"); + const targetUrl = await resetFixture("migrate-v3-conflict-target.db"); + + await createV3Database(sourceUrl, validTypeKey, { includePanelMessage: false }); + + await withClient(targetUrl, async (target) => { + await target.batch(["CREATE TABLE tickets (id INTEGER)", "INSERT INTO tickets (id) VALUES (1)"], "write"); + }); + + const result = await runMigration(["--source", sourceUrl, "--target", targetUrl], { expectFailure: true }); + + assertIncludes(result.stderr, "Target database already contains", "target conflict should fail before import."); + console.log("[migrate:v3:test] target conflict failure passed."); +} + +async function verifyConfigMigration() { + const sourcePath = path.join(fixtureDirectory, "migrate-v3-config-source.jsonc"); + const outputPath = path.join(fixtureDirectory, "migrate-v3-config-output.ts"); + + await rm(sourcePath, { force: true }); + await rm(outputPath, { force: true }); + try { + await writeFile( + sourcePath, + `{ + "clientId": "123", + "guildId": "456", + "lang": "main", + "openTicketChannelId": "789", + "ticketTypes": [ + { + "codeName": "support", + "name": "Support", + "description": "Help", + "emoji": "S", + "categoryId": "111", + "ticketNameOption": "support-TICKETCOUNT", + "customDescription": "Reason: REASON1", + "cantAccess": ["222"], + "askQuestions": true, + "questions": [ + { + "label": "What happened?", + "placeholder": "Explain", + "style": "PARAGRAPH", + "maxLength": 1000, + }, + ], + "staffRoles": ["333"], + }, + ], + "ticketNameOption": "Ticket-TICKETCOUNT", + "claimOption": { + "claimButton": true, + "nameWhenClaimed": "Claimed-X_USERNAME-TICKETCOUNT", + "categoryWhenClaimed": "444", + }, + "rolesWhoHaveAccessToTheTickets": ["333"], + "rolesWhoCanNotCreateTickets": ["555"], + "pingRoleWhenOpened": true, + "roleToPingWhenOpenedId": ["666"], + "logs": true, + "logsChannelId": "777", + "closeOption": { + "closeButton": true, + "dmUser": true, + "createTranscript": true, + "askReason": false, + "whoCanCloseTicket": "EVERYONE", + "closeTicketCategoryId": "888", + }, + "uuidType": "emoji", + "status": { + "enabled": true, + "text": "github.com/Sayrix", + "type": "WATCHING", + "url": "", + "status": "online", + }, + "maxTicketOpened": 2, + "minimalTracking": false, + "showWSLog": true, + }` + ); + + await run(process.execPath, [ + path.join(rootDirectory, "dist", "src", "migrate-v3-config.js"), + "--source", + sourcePath, + "--output", + outputPath, + "--panel-key", + "mainPanel" + ]); + + const generated = await readFile(outputPath, "utf8"); + + assertGeneratedConfig(generated, [ + ["mainPanel:", "config migration should use the requested panel key."], + ["support:", "config migration should preserve ticket type codeName keys."], + ['channelNameTemplate: "support-{ticketNumber}"', "config migration should convert ticket name tokens."], + ['welcomeContent: "Reason: {reason1}"', "config migration should convert reason tokens."], + ['nameWhenClaimed: "Claimed-{claimerUsername}-{ticketNumber}"', "config migration should convert claim tokens."], + ["staffOnly: false", "config migration should convert EVERYONE close mode."] + ]); + + console.log("[migrate:v3:test] config migration passed."); + } finally { + await rm(sourcePath, { force: true }); + await rm(outputPath, { force: true }); + } +} + +async function createV3Database(databaseUrl, ticketTypeKey, options) { + await withClient(databaseUrl, async (client) => { + await client.batch( + [ + "CREATE TABLE config (key TEXT PRIMARY KEY, value TEXT)", + ` + CREATE TABLE tickets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channelid TEXT NOT NULL UNIQUE, + messageid TEXT NOT NULL UNIQUE, + category TEXT NOT NULL, + invited TEXT NOT NULL DEFAULT '[]', + reason TEXT NOT NULL, + creator TEXT NOT NULL, + createdat BIGINT NOT NULL, + claimedby TEXT, + claimedat BIGINT, + closedby TEXT, + closedat BIGINT, + closereason TEXT, + transcript TEXT + ) + `, + ...(options.includePanelMessage + ? [ + { + sql: "INSERT INTO config (key, value) VALUES (?, ?)", + args: ["openTicketMessageId", "999999999999999999"] + } + ] + : []), + { + sql: ` + INSERT INTO tickets ( + id, + channelid, + messageid, + category, + invited, + reason, + creator, + createdat + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, + args: [ + 10, + "111111111111111111", + "111111111111111112", + JSON.stringify({ codeName: ticketTypeKey }), + JSON.stringify(["333333333333333333"]), + "Need help", + "111111111111111113", + 1710000000000 + ] + }, + { + sql: ` + INSERT INTO tickets ( + id, + channelid, + messageid, + category, + invited, + reason, + creator, + createdat, + claimedby, + claimedat, + closedby, + closedat, + closereason, + transcript + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + args: [ + 11, + "211111111111111111", + "211111111111111112", + JSON.stringify({ codeName: ticketTypeKey }), + "[]", + "Billing question", + "211111111111111113", + 1710000001000, + "222222222222222222", + 1710000002000, + "222222222222222223", + 1710000003000, + "Done", + "https://ticket.pm/transcript/abc" + ] + } + ], + "write" + ); + }); +} + +async function resetFixture(fileName) { + const filePath = path.join(fixtureDirectory, fileName); + await rm(filePath, { force: true }); + return `file:.data/${fileName}`; +} + +async function runMigration(args, options = {}) { + return run(process.execPath, [path.join(rootDirectory, "dist", "src", "migrate-v3-db.js"), ...args], options); +} + +function run(command, args, options = {}) { + return new Promise((resolvePromise, rejectPromise) => { + const child = spawn(command, args, { + cwd: rootDirectory, + env: process.env, + stdio: ["ignore", "pipe", "pipe"] + }); + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + child.on("error", rejectPromise); + child.on("exit", (code) => { + const failed = code !== 0; + + if (options.expectFailure ? failed : !failed) { + resolvePromise({ stdout, stderr, code }); + return; + } + + const output = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n"); + rejectPromise(new Error(`Command failed with exit code ${code ?? "unknown"}.\n${output}`)); + }); + }); +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function assertIncludes(value, expected, message) { + assert(value.includes(expected), message); +} + +function assertGeneratedConfig(source, expectations) { + for (const [expected, message] of expectations) { + assertIncludes(source, expected, message); + } +} + +async function withClient(databaseUrl, fn) { + const client = createClient({ url: databaseUrl }); + + try { + return await fn(client); + } finally { + client.close(); + } +} diff --git a/src/migrate-v3-config.ts b/src/migrate-v3-config.ts new file mode 100644 index 00000000..c0fadbed --- /dev/null +++ b/src/migrate-v3-config.ts @@ -0,0 +1,555 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { access, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { parseArgs as parseNodeArgs } from "node:util"; +import { parse as parseJsonc, type ParseError, printParseErrorCode } from "jsonc-parser"; + +interface CliOptions { + source: string; + output: string; + overwrite: boolean; + panelKey: string; +} + +interface V3Config { + clientId?: unknown; + guildId?: unknown; + lang?: unknown; + openTicketChannelId?: unknown; + ticketTypes?: unknown; + ticketNameOption?: unknown; + claimOption?: { + claimButton?: unknown; + nameWhenClaimed?: unknown; + categoryWhenClaimed?: unknown; + }; + rolesWhoHaveAccessToTheTickets?: unknown; + rolesWhoCanNotCreateTickets?: unknown; + pingRoleWhenOpened?: unknown; + roleToPingWhenOpenedId?: unknown; + logs?: unknown; + logsChannelId?: unknown; + closeOption?: { + closeButton?: unknown; + dmUser?: unknown; + createTranscript?: unknown; + askReason?: unknown; + whoCanCloseTicket?: unknown; + closeTicketCategoryId?: unknown; + }; + uuidType?: unknown; + status?: { + enabled?: unknown; + text?: unknown; + type?: unknown; + url?: unknown; + status?: unknown; + }; + maxTicketOpened?: unknown; + minimalTracking?: unknown; + showWSLog?: unknown; +} + +interface V3TicketType { + codeName?: unknown; + name?: unknown; + description?: unknown; + emoji?: unknown; + categoryId?: unknown; + ticketNameOption?: unknown; + customDescription?: unknown; + cantAccess?: unknown; + askQuestions?: unknown; + questions?: unknown; + staffRoles?: unknown; +} + +interface V3Question { + label?: unknown; + placeholder?: unknown; + style?: unknown; + maxLength?: unknown; +} + +interface MigrationSummary { + source: string; + output: string; + ticketTypeKeys: string[]; + panelKey: string; + warnings: string[]; +} + +interface CliValues { + source?: string; + output?: string; + "panel-key"?: string; + overwrite?: boolean; + help?: boolean; +} + +const DEFAULT_SOURCE = "config.jsonc"; +const DEFAULT_OUTPUT = "config/config.ts"; +const DEFAULT_PANEL_KEY = "support"; +const SUPPORTED_LOCALES = new Set(["en", "fr"]); + +runFromCli().catch((error) => { + console.error(`[migrate:v3:config] ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); +}); + +async function runFromCli() { + const options = parseArgs(process.argv.slice(2)); + + if (!options) { + printHelp(); + return; + } + + const summary = await migrateV3Config(options); + printSummary(summary); +} + +async function migrateV3Config(options: CliOptions): Promise { + const sourcePath = path.resolve(options.source); + const outputPath = path.resolve(options.output); + + if (!options.overwrite && (await fileExists(outputPath))) { + throw new Error(`Output already exists: ${options.output}. Pass --overwrite to replace it.`); + } + + const source = await readFile(sourcePath, "utf8"); + const warnings: string[] = []; + const config = parseV3Jsonc(source) as V3Config; + const migrated = migrateConfig(config, options.panelKey, warnings); + const rendered = renderConfigFile(migrated); + + await writeFile(outputPath, rendered); + + return { + source: options.source, + output: options.output, + ticketTypeKeys: Object.keys(migrated.ticketTypes), + panelKey: options.panelKey, + warnings + }; +} + +function parseArgs(args: string[]): CliOptions | null { + const { values } = parseNodeArgs({ + args, + allowPositionals: false, + options: { + source: { + type: "string", + default: DEFAULT_SOURCE + }, + output: { + type: "string", + default: DEFAULT_OUTPUT + }, + "panel-key": { + type: "string", + default: DEFAULT_PANEL_KEY + }, + overwrite: { + type: "boolean", + default: false + }, + help: { + type: "boolean", + short: "h" + } + } + }) as { values: CliValues }; + + if (values.help) { + return null; + } + + return { + source: values.source ?? DEFAULT_SOURCE, + output: values.output ?? DEFAULT_OUTPUT, + overwrite: values.overwrite ?? false, + panelKey: values["panel-key"] ?? DEFAULT_PANEL_KEY + }; +} + +function parseV3Jsonc(source: string) { + const errors: ParseError[] = []; + const parsed = parseJsonc(source, errors, { + allowTrailingComma: true + }); + + if (errors.length > 0) { + const firstError = errors[0]; + throw new Error(`Failed to parse v3 JSONC config: ${printParseErrorCode(firstError.error)} at offset ${firstError.offset}.`); + } + + return parsed; +} + +function migrateConfig(config: V3Config, panelKey: string, warnings: string[]) { + const ticketTypes = readTicketTypes(config.ticketTypes).map((ticketType) => migrateTicketType(ticketType)); + const ticketTypeRecord = Object.fromEntries(ticketTypes.map((ticketType) => [ticketType.key, ticketType.value])); + + if (ticketTypes.length === 0) { + throw new Error("The v3 config must contain at least one ticket type."); + } + + const lang = migrateLocale(readOptionalString(config.lang), warnings); + const mentionRoleIds = readOptionalBoolean(config.pingRoleWhenOpened, false) + ? readStringArray(config.roleToPingWhenOpenedId, "roleToPingWhenOpenedId") + : []; + + return { + clientId: readRequiredString(config.clientId, "clientId"), + guildId: readRequiredString(config.guildId, "guildId"), + lang, + uuidType: readUuidType(config.uuidType), + minimalTracking: readOptionalBoolean(config.minimalTracking, false), + showWSLog: readOptionalBoolean(config.showWSLog, false), + logs: { + enabled: readOptionalBoolean(config.logs, false), + channelId: readOptionalString(config.logsChannelId) ?? "" + }, + status: migrateStatus(config.status), + tickets: { + channelNameTemplate: migrateTemplate(readOptionalString(config.ticketNameOption) || "ticket-{ticketNumber}"), + maxOpenPerUser: readOptionalNumber(config.maxTicketOpened, 0), + staffRoleIds: readStringArray(config.rolesWhoHaveAccessToTheTickets, "rolesWhoHaveAccessToTheTickets"), + blockedRoleIds: readStringArray(config.rolesWhoCanNotCreateTickets, "rolesWhoCanNotCreateTickets"), + mentionRoleIds, + defaultWelcomeMessage: "tickets/ticket-opened", + defaultWelcomeContent: "", + claims: migrateClaims(config.claimOption), + close: migrateClose(config.closeOption) + }, + panels: { + [panelKey]: { + channelId: readRequiredString(config.openTicketChannelId, "openTicketChannelId"), + message: "tickets/open-panel", + content: "", + opener: { + type: "inline-select", + ticketTypes: ticketTypes.map((ticketType) => ticketType.key), + placeholder: "Open a ticket" + } + } + }, + ticketTypes: ticketTypeRecord + }; +} + +function readTicketTypes(value: unknown) { + if (!Array.isArray(value)) { + throw new Error("Expected ticketTypes to be an array."); + } + + return value as V3TicketType[]; +} + +function migrateTicketType(ticketType: V3TicketType) { + const key = readRequiredString(ticketType.codeName, "ticketTypes[].codeName"); + const migrated: Record = { + name: readRequiredString(ticketType.name, `ticketTypes.${key}.name`), + description: readOptionalString(ticketType.description) ?? "", + emoji: readOptionalString(ticketType.emoji) ?? undefined, + categoryId: readRequiredString(ticketType.categoryId, `ticketTypes.${key}.categoryId`), + channelNameTemplate: migrateTemplate(readOptionalString(ticketType.ticketNameOption) || "{ticketNumber}-ticket-{username}"), + message: "tickets/ticket-opened", + welcomeContent: migrateTemplate(readOptionalString(ticketType.customDescription) ?? ""), + blockedRoleIds: readStringArray(ticketType.cantAccess, `ticketTypes.${key}.cantAccess`), + staffRoleIds: readStringArray(ticketType.staffRoles, `ticketTypes.${key}.staffRoles`) + }; + + if (!migrated.emoji) { + delete migrated.emoji; + } + + if (readOptionalBoolean(ticketType.askQuestions, false)) { + migrated.openForm = { + title: `${migrated.name} Ticket`, + questions: readQuestions(ticketType.questions, key) + }; + } + + return { + key, + value: migrated + }; +} + +function readQuestions(value: unknown, ticketTypeKey: string) { + if (!Array.isArray(value) || value.length === 0) { + throw new Error(`ticketTypes.${ticketTypeKey} enables askQuestions but has no questions.`); + } + + return (value as V3Question[]).map((question, index) => ({ + key: `reason${index + 1}`, + label: readRequiredString(question.label, `ticketTypes.${ticketTypeKey}.questions.${index}.label`), + placeholder: readOptionalString(question.placeholder) ?? undefined, + style: migrateQuestionStyle(readOptionalString(question.style)), + required: true, + maxLength: readOptionalNumber(question.maxLength, 1000) + })); +} + +function migrateClaims(claimOption: V3Config["claimOption"]) { + const claimButton = readOptionalBoolean(claimOption?.claimButton, false); + const categoryWhenClaimed = readOptionalString(claimOption?.categoryWhenClaimed); + + return { + enabled: claimButton, + mode: "soft", + showButtons: claimButton, + allowUnclaim: true, + nameWhenClaimed: migrateClaimTemplate(readOptionalString(claimOption?.nameWhenClaimed) || "{ticketNumber}-claimed-{claimerUsername}"), + categoryWhenClaimed: categoryWhenClaimed || undefined, + takeoverMode: "staff", + takeoverRoleIds: [] + }; +} + +function migrateClose(closeOption: V3Config["closeOption"]) { + const closeTicketCategoryId = readOptionalString(closeOption?.closeTicketCategoryId); + + return { + staffOnly: readOptionalString(closeOption?.whoCanCloseTicket) !== "EVERYONE", + dmUserOnClose: readOptionalBoolean(closeOption?.dmUser, true), + askForReason: readOptionalBoolean(closeOption?.askReason, true), + showCloseButton: readOptionalBoolean(closeOption?.closeButton, true), + deleteChannelOnClose: false, + createTranscript: readOptionalBoolean(closeOption?.createTranscript, true), + closeTicketCategoryId: closeTicketCategoryId || undefined, + dmMessage: "tickets/ticket-closed-dm", + channelMessage: "tickets/ticket-closed" + }; +} + +function migrateStatus(status: V3Config["status"]) { + if (!status) { + return { + enabled: false, + status: "online" + }; + } + + return { + enabled: readOptionalBoolean(status.enabled, false), + text: readOptionalString(status.text) ?? undefined, + type: migrateStatusType(readOptionalString(status.type)), + url: readOptionalString(status.url) ?? undefined, + status: migrateStatusValue(readOptionalString(status.status)) + }; +} + +function migrateLocale(value: string | undefined, warnings: string[]) { + if (!value || value === "main") { + return "en"; + } + + if (SUPPORTED_LOCALES.has(value)) { + return value; + } + + warnings.push(`Unsupported v3 lang "${value}" was migrated to "en".`); + return "en"; +} + +function migrateStatusType(value: string | undefined) { + const allowed = new Set(["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM", "COMPETING"]); + return value && allowed.has(value) ? value : "PLAYING"; +} + +function migrateStatusValue(value: string | undefined) { + const allowed = new Set(["online", "idle", "dnd", "invisible"]); + return value && allowed.has(value) ? value : "online"; +} + +function readUuidType(value: unknown) { + return value === "emoji" ? "emoji" : "uuid"; +} + +function migrateQuestionStyle(value: string | undefined) { + return value === "PARAGRAPH" || value === "paragraph" ? "paragraph" : "short"; +} + +function migrateTemplate(value: string) { + return value + .replaceAll("TICKETCOUNT", "{ticketNumber}") + .replaceAll("CATEGORYNAME", "{ticketTypeName}") + .replaceAll("USERNAME", "{username}") + .replaceAll("USERID", "{userId}") + .replace(/\bREASON(\d+)\b/gu, "{reason$1}") + .replace(/\bREASON\b/gu, "{reason}"); +} + +function migrateClaimTemplate(value: string) { + return migrateTemplate(value.replaceAll("X_USERNAME", "{claimerUsername}").replaceAll("X_USERID", "{claimerId}")); +} + +function readRequiredString(value: unknown, label: string) { + const stringValue = readOptionalString(value); + + if (stringValue === undefined) { + throw new Error(`Missing required string value: ${label}.`); + } + + return stringValue; +} + +function readOptionalString(value: unknown) { + return typeof value === "string" ? value : undefined; +} + +function readOptionalBoolean(value: unknown, fallback: boolean) { + return typeof value === "boolean" ? value : fallback; +} + +function readOptionalNumber(value: unknown, fallback: number) { + return typeof value === "number" && Number.isFinite(value) ? value : fallback; +} + +function readStringArray(value: unknown, label: string) { + if (value === undefined) { + return []; + } + + if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) { + throw new Error(`Expected ${label} to be an array of strings.`); + } + + return value; +} + +function renderConfigFile(config: ReturnType) { + return `${licenseHeader()}import { defineConfig } from "@/config/index.js"; + +export default defineConfig("0.0.1", ${formatValue(config, 0)}); +${licenseHeader()}`; +} + +function formatValue(value: unknown, depth: number): string { + if (value === undefined) { + return "undefined"; + } + + if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return JSON.stringify(value); + } + + if (Array.isArray(value)) { + if (value.length === 0) { + return "[]"; + } + + const entries = value.map((entry) => `${indent(depth + 1)}${formatValue(entry, depth + 1)}`); + return `[\n${entries.join(",\n")}\n${indent(depth)}]`; + } + + if (typeof value === "object") { + const entries = Object.entries(value).filter(([, entry]) => entry !== undefined); + + if (entries.length === 0) { + return "{}"; + } + + return `{\n${entries + .map(([key, entry]) => `${indent(depth + 1)}${formatKey(key)}: ${formatValue(entry, depth + 1)}`) + .join(",\n")}\n${indent(depth)}}`; + } + + throw new Error(`Unsupported config value type: ${typeof value}.`); +} + +function formatKey(key: string) { + return /^[A-Za-z_$][\w$]*$/u.test(key) ? key : JSON.stringify(key); +} + +function indent(depth: number) { + return "\t".repeat(depth); +} + +async function fileExists(filePath: string) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +function printSummary(summary: MigrationSummary) { + console.log("[migrate:v3:config] Config migration complete."); + console.log(` Source: ${summary.source}`); + console.log(` Output: ${summary.output}`); + console.log(` Panel key: ${summary.panelKey}`); + console.log(` Ticket types: ${summary.ticketTypeKeys.join(", ")}`); + + for (const warning of summary.warnings) { + console.warn(` Warning: ${warning}`); + } +} + +function printHelp() { + console.log(` +Usage: + bun run migrate:v3:config -- --source config.jsonc --output config/config.ts + bun run migrate:v3:config -- --source config.jsonc --output .data/config.v4.ts + +Options: + --source v3 JSONC config path. Defaults to config.jsonc. + --output v4 TypeScript config path. Defaults to config/config.ts. + --panel-key v4 panel key created from openTicketChannelId. Defaults to support. + --overwrite Replace the output file if it already exists. + --help Show this help. +`.trim()); +} + +function licenseHeader() { + return `/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +`; +} + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ diff --git a/src/migrate-v3-db.ts b/src/migrate-v3-db.ts new file mode 100644 index 00000000..0116f4c6 --- /dev/null +++ b/src/migrate-v3-db.ts @@ -0,0 +1,657 @@ +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ + +import { access } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseArgs as parseNodeArgs } from "node:util"; +import { createClient, type Client, type InStatement, type InValue, type Row, type Value } from "@libsql/client/sqlite3"; +import { config as loadEnv } from "dotenv"; +import botConfig from "../config/config.js"; + +interface CliOptions { + source: string; + target: string; + overwrite: boolean; + panelKey?: string; + typeMap: Map; +} + +interface V3TicketRow { + id: number; + channelId: string; + creationMessageId: string; + type: string; + reason: string | null; + createdBy: string; + createdAt: number; + claimedAt: number | null; + claimedBy: string | null; + invitedUserIds: string; + closedAt: number | null; + closedBy: string | null; + closedReason: string | null; + transcriptUrl: string | null; +} + +interface PanelMigration { + row?: { + panelKey: string; + channelId: string; + messageId: string; + }; + skippedReason?: string; +} + +interface MigrationSummary { + source: string; + target: string; + ticketsMigrated: number; + panelMigration: PanelMigration; + typeMap: Map; + overwrite: boolean; +} + +interface CliValues { + source?: string; + target?: string; + "panel-key"?: string; + "type-map"?: string[]; + overwrite?: boolean; + help?: boolean; +} + +const CREATE_TICKETS_TABLE = ` +CREATE TABLE IF NOT EXISTS tickets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channelId TEXT NOT NULL UNIQUE, + creationMessageId TEXT NOT NULL UNIQUE, + type TEXT NOT NULL, + reason TEXT, + createdBy TEXT NOT NULL, + createdAt INTEGER NOT NULL, + claimedAt INTEGER, + claimedBy TEXT, + invitedUserIds TEXT NOT NULL DEFAULT '[]', + closedAt INTEGER, + closedBy TEXT, + closedReason TEXT, + transcriptUrl TEXT +)`; + +const CREATE_PANEL_MESSAGES_TABLE = ` +CREATE TABLE IF NOT EXISTS panel_messages ( + panelKey TEXT PRIMARY KEY, + channelId TEXT NOT NULL, + messageId TEXT NOT NULL, + updatedAt INTEGER NOT NULL +)`; + +const CREATE_APP_META_TABLE = ` +CREATE TABLE IF NOT EXISTS app_meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updatedAt INTEGER NOT NULL +)`; + +loadEnv({ path: "./config/.env", quiet: true }); + +runFromCli().catch((error) => { + console.error(`[migrate:v3] ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); +}); + +async function runFromCli() { + const options = parseArgs(process.argv.slice(2)); + + if (!options) { + printHelp(); + return; + } + + const summary = await migrateV3Database(options); + printSummary(summary); +} + +async function migrateV3Database(options: CliOptions): Promise { + const sourceUrl = normalizeSqliteUrl(options.source, "source"); + const targetUrl = normalizeSqliteUrl(options.target, "target"); + + await assertSourceFileExists(sourceUrl); + validatePanelKey(options.panelKey); + validateTypeMapTargets(options.typeMap); + + const source = createClient({ url: sourceUrl }); + const target = createClient({ url: targetUrl }); + + try { + await assertV3SourceSchema(source); + + const tickets = await readV3Tickets(source, options.typeMap); + const panelMigration = await preparePanelMigration(source, options.panelKey); + + await ensureV4Schema(target); + await assertTargetCanBeUsed(target, options.overwrite); + await writeV4Data(target, tickets, panelMigration, options.overwrite); + + return { + source: sourceUrl, + target: targetUrl, + ticketsMigrated: tickets.length, + panelMigration, + typeMap: options.typeMap, + overwrite: options.overwrite + }; + } finally { + source.close(); + target.close(); + } +} + +function parseArgs(args: string[]): CliOptions | null { + const { values } = parseNodeArgs({ + args, + allowPositionals: false, + options: { + source: { + type: "string" + }, + target: { + type: "string" + }, + "panel-key": { + type: "string" + }, + "type-map": { + type: "string", + multiple: true + }, + overwrite: { + type: "boolean", + default: false + }, + help: { + type: "boolean", + short: "h" + } + } + }) as { values: CliValues }; + + if (values.help) { + return null; + } + + if (!values.source) { + throw new Error("Missing required --source option."); + } + + const target = values.target ?? process.env.DB_FILE_NAME; + + if (!target) { + throw new Error("Missing target database. Set DB_FILE_NAME in config/.env or pass --target."); + } + + return { + source: values.source, + target, + overwrite: values.overwrite ?? false, + panelKey: values["panel-key"], + typeMap: parseTypeMap(values["type-map"] ?? []) + }; +} + +function parseTypeMap(mappings: string[]) { + const typeMap = new Map(); + + for (const mapping of mappings) { + const separatorIndex = mapping.indexOf("="); + + if (separatorIndex <= 0 || separatorIndex === mapping.length - 1) { + throw new Error(`Invalid --type-map "${mapping}". Expected oldKey=newKey.`); + } + + typeMap.set(mapping.slice(0, separatorIndex), mapping.slice(separatorIndex + 1)); + } + + return typeMap; +} + +function normalizeSqliteUrl(value: string, label: "source" | "target") { + const trimmed = value.trim(); + + if (!trimmed) { + throw new Error(`The ${label} database URL is empty.`); + } + + if (/^[a-z][a-z0-9+.-]*:/iu.test(trimmed)) { + if (!trimmed.startsWith("file:")) { + throw new Error(`The ${label} database must be a local SQLite file URL, for example file:.data/sqlite.db.`); + } + + return trimmed; + } + + return `file:${trimmed}`; +} + +async function assertSourceFileExists(sourceUrl: string) { + const sourcePath = localFilePath(sourceUrl); + + if (!sourcePath) { + throw new Error("The v3 source database must be a local SQLite file URL, for example file:./tixbot.db."); + } + + try { + await access(sourcePath); + } catch { + throw new Error(`Source database file does not exist: ${sourcePath}`); + } +} + +function localFilePath(databaseUrl: string) { + if (!databaseUrl.startsWith("file:")) { + return null; + } + + const value = databaseUrl.slice("file:".length); + + if (value.startsWith("//")) { + return fileURLToPath(databaseUrl); + } + + return path.resolve(value); +} + +function validatePanelKey(panelKey: string | undefined) { + if (!panelKey) { + return; + } + + if (!Object.hasOwn(botConfig.panels, panelKey)) { + throw new Error(`Unknown v4 panel key "${panelKey}". Check config/config.ts.`); + } +} + +function validateTypeMapTargets(typeMap: Map) { + for (const [oldKey, newKey] of typeMap) { + if (!Object.hasOwn(botConfig.ticketTypes, newKey)) { + throw new Error(`Invalid --type-map ${oldKey}=${newKey}. The target v4 ticket type does not exist.`); + } + } +} + +async function assertV3SourceSchema(source: Client) { + if (!(await tableExists(source, "tickets"))) { + throw new Error('The source database does not contain a v3 "tickets" table.'); + } +} + +async function tableExists(client: Client, tableName: string) { + const result = await client.execute({ + sql: "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", + args: [tableName] + }); + + return result.rows.length > 0; +} + +async function readV3Tickets(source: Client, typeMap: Map) { + const result = await source.execute(` + SELECT + id, + channelid, + messageid, + category, + invited, + reason, + creator, + createdat, + claimedby, + claimedat, + closedby, + closedat, + closereason, + transcript + FROM tickets + ORDER BY id ASC + `); + + return result.rows.map((row) => mapV3Ticket(row, typeMap)); +} + +function mapV3Ticket(row: Row, typeMap: Map): V3TicketRow { + const rawType = readTicketType(row); + const mappedType = typeMap.get(rawType) ?? rawType; + + if (!Object.hasOwn(botConfig.ticketTypes, mappedType)) { + throw new Error( + `Unknown v3 ticket type "${rawType}" for ticket ${readInteger(row, "id")}. ` + + `Add a matching v4 ticketTypes entry or pass --type-map ${rawType}=.` + ); + } + + return { + id: readInteger(row, "id"), + channelId: readText(row, "channelid"), + creationMessageId: readText(row, "messageid"), + type: mappedType, + reason: readOptionalText(row, "reason"), + createdBy: readText(row, "creator"), + createdAt: readInteger(row, "createdat"), + claimedAt: readOptionalInteger(row, "claimedat"), + claimedBy: readOptionalText(row, "claimedby"), + invitedUserIds: readInvitedUserIds(row), + closedAt: readOptionalInteger(row, "closedat"), + closedBy: readOptionalText(row, "closedby"), + closedReason: readOptionalText(row, "closereason"), + transcriptUrl: readOptionalText(row, "transcript") + }; +} + +function readTicketType(row: Row) { + const category = readText(row, "category"); + let parsed: unknown; + + try { + parsed = JSON.parse(category); + } catch { + throw new Error(`Ticket ${readInteger(row, "id")} has invalid category JSON.`); + } + + if (!parsed || typeof parsed !== "object" || !("codeName" in parsed) || typeof parsed.codeName !== "string") { + throw new Error(`Ticket ${readInteger(row, "id")} category JSON does not contain a string codeName.`); + } + + return parsed.codeName; +} + +function readInvitedUserIds(row: Row) { + const invited = readOptionalText(row, "invited") ?? "[]"; + let parsed: unknown; + + try { + parsed = JSON.parse(invited); + } catch { + throw new Error(`Ticket ${readInteger(row, "id")} has invalid invited JSON.`); + } + + if (!Array.isArray(parsed) || !parsed.every((entry) => typeof entry === "string")) { + throw new Error(`Ticket ${readInteger(row, "id")} invited value must be a JSON array of strings.`); + } + + return JSON.stringify(parsed); +} + +async function preparePanelMigration(source: Client, requestedPanelKey: string | undefined): Promise { + const openTicketMessageId = await readOpenTicketMessageId(source); + + if (!openTicketMessageId) { + return { + skippedReason: 'v3 config.openTicketMessageId was not found.' + }; + } + + const panelKeys = Object.keys(botConfig.panels); + const panelKey = requestedPanelKey ?? (panelKeys.length === 1 ? panelKeys[0] : undefined); + + if (!panelKey) { + return { + skippedReason: + panelKeys.length === 0 + ? "config/config.ts does not define any v4 panels." + : "config/config.ts defines multiple v4 panels; pass --panel-key to choose one." + }; + } + + const panel = botConfig.panels[panelKey]; + + return { + row: { + panelKey, + channelId: panel.channelId, + messageId: openTicketMessageId + } + }; +} + +async function readOpenTicketMessageId(source: Client) { + if (!(await tableExists(source, "config"))) { + return null; + } + + const result = await source.execute({ + sql: "SELECT value FROM config WHERE key = ? LIMIT 1", + args: ["openTicketMessageId"] + }); + const row = result.rows[0]; + + if (!row) { + return null; + } + + return readOptionalText(row, "value"); +} + +async function ensureV4Schema(target: Client) { + await target.batch([CREATE_TICKETS_TABLE, CREATE_PANEL_MESSAGES_TABLE, CREATE_APP_META_TABLE], "write"); +} + +async function assertTargetCanBeUsed(target: Client, overwrite: boolean) { + if (overwrite) { + return; + } + + const ticketCount = await countRows(target, "tickets"); + const panelMessageCount = await countRows(target, "panel_messages"); + + if (ticketCount > 0 || panelMessageCount > 0) { + throw new Error( + `Target database already contains ${ticketCount} ticket row(s) and ${panelMessageCount} panel row(s). ` + + "Pass --overwrite to clear those v4 tables before migrating." + ); + } +} + +async function countRows(client: Client, tableName: "tickets" | "panel_messages") { + const result = await client.execute(`SELECT COUNT(*) AS count FROM ${tableName}`); + const count = result.rows[0]?.count; + + return Number(count ?? 0); +} + +async function writeV4Data(target: Client, tickets: V3TicketRow[], panelMigration: PanelMigration, overwrite: boolean) { + const statements: InStatement[] = []; + + if (overwrite) { + statements.push("DELETE FROM tickets", "DELETE FROM panel_messages"); + } + + for (const ticket of tickets) { + statements.push({ + sql: ` + INSERT INTO tickets ( + id, + channelId, + creationMessageId, + type, + reason, + createdBy, + createdAt, + claimedAt, + claimedBy, + invitedUserIds, + closedAt, + closedBy, + closedReason, + transcriptUrl + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + args: [ + ticket.id, + ticket.channelId, + ticket.creationMessageId, + ticket.type, + ticket.reason, + ticket.createdBy, + ticket.createdAt, + ticket.claimedAt, + ticket.claimedBy, + ticket.invitedUserIds, + ticket.closedAt, + ticket.closedBy, + ticket.closedReason, + ticket.transcriptUrl + ] satisfies InValue[] + }); + } + + if (panelMigration.row) { + statements.push({ + sql: ` + INSERT INTO panel_messages (panelKey, channelId, messageId, updatedAt) + VALUES (?, ?, ?, ?) + `, + args: [panelMigration.row.panelKey, panelMigration.row.channelId, panelMigration.row.messageId, Date.now()] + }); + } + + if (statements.length > 0) { + await target.batch(statements, "write"); + } +} + +function readText(row: Row, column: string) { + const value = readValue(row, column); + + if (typeof value !== "string") { + throw new Error(`Expected "${column}" to be text.`); + } + + return value; +} + +function readOptionalText(row: Row, column: string) { + const value = readValue(row, column); + + if (value === null || value === undefined) { + return null; + } + + if (typeof value !== "string") { + throw new Error(`Expected "${column}" to be text or null.`); + } + + return value; +} + +function readInteger(row: Row, column: string) { + const value = readValue(row, column); + const numberValue = valueToNumber(value, column); + + if (!Number.isSafeInteger(numberValue)) { + throw new Error(`Expected "${column}" to be a safe integer.`); + } + + return numberValue; +} + +function readOptionalInteger(row: Row, column: string) { + const value = readValue(row, column); + + if (value === null || value === undefined) { + return null; + } + + const numberValue = valueToNumber(value, column); + + if (!Number.isSafeInteger(numberValue)) { + throw new Error(`Expected "${column}" to be a safe integer or null.`); + } + + return numberValue; +} + +function valueToNumber(value: Value | undefined, column: string) { + if (typeof value === "number") { + return value; + } + + if (typeof value === "bigint") { + return Number(value); + } + + if (typeof value === "string" && /^-?\d+$/u.test(value)) { + return Number(value); + } + + throw new Error(`Expected "${column}" to be an integer.`); +} + +function readValue(row: Row, column: string) { + return row[column] as Value | undefined; +} + +function printSummary(summary: MigrationSummary) { + console.log("[migrate:v3] Migration complete."); + console.log(` Source: ${summary.source}`); + console.log(` Target: ${summary.target}`); + console.log(` Tickets migrated: ${summary.ticketsMigrated}`); + + if (summary.panelMigration.row) { + console.log(` Panel migrated: ${summary.panelMigration.row.panelKey}`); + } else { + console.log(` Panel skipped: ${summary.panelMigration.skippedReason ?? "not requested"}`); + } + + if (summary.typeMap.size > 0) { + console.log(` Type map: ${[...summary.typeMap].map(([oldKey, newKey]) => `${oldKey}=${newKey}`).join(", ")}`); + } else { + console.log(" Type map: none"); + } + + console.log(` Overwrite: ${summary.overwrite ? "yes" : "no"}`); +} + +function printHelp() { + console.log(` +Usage: + bun run migrate:v3 -- --source file:./tixbot.db + bun run migrate:v3 -- --source file:./tixbot.db --target file:.data/sqlite.db + +Options: + --source Required v3 SQLite database file. + --target v4 database. Defaults to DB_FILE_NAME from config/.env. + --panel-key v4 panel key for the old open ticket message. + --type-map old=new Map a v3 ticket type codeName to a v4 ticketTypes key. Can be repeated. + --overwrite Clear v4 tickets and panel_messages before importing. + --help Show this help. +`.trim()); +} + +/* +Ticket-Bot is licensed under the GNU Affero General Public License, +version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. + +Additional Term under GNU AGPL v3, Section 7(b): + +You are required to preserve and display, in a location clearly visible +to end users interacting with the bot (such as bot embeds, the bot's +"Bio" Discord profile, status, or equivalent), a notice that the +software is powered by Ticket-Bot, including a link to the original +project repository or to its website. + +This notice must not be removed, obscured, or replaced. +*/ From 24bf44285b08f4b506f0101aa1794d618808948e Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 25 Apr 2026 14:20:43 +0200 Subject: [PATCH 45/67] feat(config): update .gitignore to include config files and add example.env --- .gitignore | 4 ++-- config/{.env.example => example.env} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename config/{.env.example => example.env} (100%) diff --git a/.gitignore b/.gitignore index 4768ac10..f8a6d39a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ node_modules/ dist/ -config/.env -config/config.ts +config/.env* +config/config.ts* .data/*.db \ No newline at end of file diff --git a/config/.env.example b/config/example.env similarity index 100% rename from config/.env.example rename to config/example.env From 55256427328af8827fc1a861919e6272ca681154 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 25 Apr 2026 15:28:56 +0200 Subject: [PATCH 46/67] fix(panel): clear select interaction state on error * Acknowledge panel select menu component with empty update. * Send ephemeral error messages via follow-up. --- src/features/tickets/panel-sync.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/features/tickets/panel-sync.ts b/src/features/tickets/panel-sync.ts index aa9220a0..e6dac1b9 100644 --- a/src/features/tickets/panel-sync.ts +++ b/src/features/tickets/panel-sync.ts @@ -23,7 +23,7 @@ import type { import { ComponentType, MessageFlags } from "@discordjs/core"; import { eq } from "drizzle-orm"; import { createCustomId } from "@/core/custom-id"; -import { reply } from "@/core/respond"; +import { followUp, reply, updateMessage } from "@/core/respond"; import type { BotApp, ComponentExecutionContext } from "@/core/types"; import { panelMessagesTable } from "@/db/schema"; import { @@ -99,7 +99,8 @@ export async function handlePanelSelect(context: ComponentExecutionContext, inte const ticketTypeKey = values[0]; if (!ticketTypeKey) { - await reply(context.app, interaction, { + await updateMessage(context.app, interaction, {}); + await followUp(context.app, interaction, { content: context.app.LL.tickets.panel.select_type(), flags: MessageFlags.Ephemeral }); @@ -109,7 +110,8 @@ export async function handlePanelSelect(context: ComponentExecutionContext, inte const allowedTicketTypes = new Set(getPanelTicketTypeKeys(panel)); if (!allowedTicketTypes.has(ticketTypeKey)) { - await reply(context.app, interaction, { + await updateMessage(context.app, interaction, {}); + await followUp(context.app, interaction, { content: context.app.LL.tickets.panel.unavailable_type(), flags: MessageFlags.Ephemeral }); From 963340e2d546bbab19aef63d97f1b55d7f5cc88a Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sat, 25 Apr 2026 15:31:29 +0200 Subject: [PATCH 47/67] fix(tickets): force UI component refresh on workflow * Reset string select menu state before validating ticket access. * Re-render source message components sequentially on modal forms. * Strip lingering default choices inside \clearActionRowDefaults\. * Encapsulate reply methodology into \ espondToTicketOpen\. --- src/features/tickets/ticket-workflow.ts | 166 ++++++++++++++++++++---- 1 file changed, 141 insertions(+), 25 deletions(-) diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index 37bbcb37..bd512735 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -15,13 +15,16 @@ This notice must not be removed, obscured, or replaced. import type { APIActionRowComponent, + APIAutoPopulatedSelectMenuComponent, APIButtonComponentWithCustomId, - APIMessageTopLevelComponent, + APIComponentInMessageActionRow, APIMessageComponentInteraction, + APIMessageTopLevelComponent, APIModalSubmitInteraction, - APIModalSubmitTextInputComponent + APIModalSubmitTextInputComponent, + APISelectMenuComponent, + APIStringSelectComponent } from "@discordjs/core"; -import type { APIComponentInContainer, APIContainerComponent } from "discord-api-types/v10"; import { ButtonStyle, ChannelType, @@ -31,6 +34,7 @@ import { PermissionFlagsBits, TextInputStyle } from "@discordjs/core"; +import type { APIComponentInContainer, APIContainerComponent } from "discord-api-types/v10"; import { and, count, eq, isNull } from "drizzle-orm"; import { createCustomId } from "@/core/custom-id"; import { deferReply, editReply, followUp, reply, showModal, updateMessage } from "@/core/respond"; @@ -66,6 +70,9 @@ interface TicketOpenReasonData { combined: string; } +type TicketOpenResponseMode = "initial" | "follow-up"; +type MessageActionRow = APIActionRowComponent; + export async function handleOpenFormSubmit(context: ComponentExecutionContext, interaction: APIModalSubmitInteraction) { const ticketTypeKey = context.route.state[0]; @@ -86,12 +93,16 @@ export async function continueTicketOpen(app: BotApp, interaction: APIMessageCom const ticketType = getTicketType(app, context.ticketTypeKey); const panel = context.panelKey ? getPanel(app, context.panelKey) : null; const roleIds = getMemberRoleIds(interaction); + const openForm = ticketType.openForm; + const hasOpenForm = Boolean(openForm?.questions.length); + const isStringSelect = interaction.data.component_type === ComponentType.StringSelect; if (!userCanAccessTicketType(app, ticketType, roleIds)) { - await reply(app, interaction, { - content: app.LL.tickets.open.not_allowed_type(), - flags: MessageFlags.Ephemeral - }); + if (isStringSelect) { + await updateMessage(app, interaction, {}); + } + + await respondToTicketOpen(app, interaction, app.LL.tickets.open.not_allowed_type(), isStringSelect ? "follow-up" : "initial"); return; } @@ -99,46 +110,151 @@ export async function continueTicketOpen(app: BotApp, interaction: APIMessageCom const allowedTypes = new Set(getPanelTicketTypeKeys(panel)); if (!allowedTypes.has(context.ticketTypeKey)) { - await reply(app, interaction, { - content: app.LL.tickets.open.unavailable_type(), - flags: MessageFlags.Ephemeral - }); + if (isStringSelect) { + await updateMessage(app, interaction, {}); + } + + await respondToTicketOpen( + app, + interaction, + app.LL.tickets.open.unavailable_type(), + isStringSelect ? "follow-up" : "initial" + ); return; } } + const resetBeforeTicketWork = isStringSelect && !hasOpenForm; + + if (resetBeforeTicketWork) { + await updateMessage(app, interaction, {}); + } + const currentOpenCount = await getUserOpenTicketCount(app, getInteractionUser(interaction).id); if (app.config.tickets.maxOpenPerUser > 0 && currentOpenCount >= app.config.tickets.maxOpenPerUser) { - await reply(app, interaction, { - content: app.LL.tickets.open.max_open_reached({ limit: app.config.tickets.maxOpenPerUser }), - flags: MessageFlags.Ephemeral - }); + if (isStringSelect && hasOpenForm) { + await updateMessage(app, interaction, {}); + await respondToTicketOpen( + app, + interaction, + app.LL.tickets.open.max_open_reached({ limit: app.config.tickets.maxOpenPerUser }), + "follow-up" + ); + return; + } + + await respondToTicketOpen( + app, + interaction, + app.LL.tickets.open.max_open_reached({ limit: app.config.tickets.maxOpenPerUser }), + resetBeforeTicketWork ? "follow-up" : "initial" + ); return; } - if (ticketType.openForm?.questions.length) { + if (openForm?.questions.length) { await showModal(app, interaction, { custom_id: createCustomId("tickets", "submit-open-form", context.ticketTypeKey), - title: ticketType.openForm.title, - components: ticketType.openForm.questions.map((question) => ({ + title: openForm.title, + components: openForm.questions.map((question) => ({ type: ComponentType.ActionRow, components: [createQuestionInput(question)] })) }); + + if (isStringSelect) { + await refreshSourceMessageComponents(app, interaction); + } + return; } - if (interaction.data.component_type === ComponentType.StringSelect) { - // Update the open panel message so that it resets the selection of the user, letting them open another ticket later. - await updateMessage(app, interaction, {}); - await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason(app), { - responseMode: "follow-up" - }); + await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason(app), { + responseMode: resetBeforeTicketWork ? "follow-up" : "deferred-reply" + }); +} + +async function respondToTicketOpen( + app: BotApp, + interaction: APIMessageComponentInteraction, + content: string, + responseMode: TicketOpenResponseMode +) { + const body = { + content, + flags: MessageFlags.Ephemeral + }; + + if (responseMode === "follow-up") { + await followUp(app, interaction, body); + return; + } + + await reply(app, interaction, body); +} + +async function refreshSourceMessageComponents(app: BotApp, interaction: APIMessageComponentInteraction) { + if (!interaction.channel.id || !interaction.message.components?.length) { return; } - await createTicket(app, interaction, context.ticketTypeKey, ticketType, createDefaultTicketOpenReason(app)); + try { + await app.client.api.channels.editMessage(interaction.channel.id, interaction.message.id, { + components: clearSelectDefaults(interaction.message.components) + }); + } catch (error) { + app.logger.warn("Failed to refresh ticket panel select menu after opening modal.", error); + } +} + +function clearSelectDefaults(components: APIMessageTopLevelComponent[]): APIMessageTopLevelComponent[] { + return components.map((component) => { + if (component.type === ComponentType.ActionRow) { + return clearActionRowDefaults(component); + } + + if (component.type === ComponentType.Container) { + return { + ...component, + components: component.components.map((child) => + child.type === ComponentType.ActionRow ? clearActionRowDefaults(child) : child + ) + }; + } + + return component; + }); +} + +function clearActionRowDefaults(row: MessageActionRow): MessageActionRow { + return { + ...row, + components: row.components.map((component) => { + if (!isSelectMenuComponent(component)) { + return component; + } + + if (component.type === ComponentType.StringSelect) { + return { + ...component, + options: component.options.map((option) => ({ + ...option, + default: false + })) + } satisfies APIStringSelectComponent; + } + + return { + ...component, + default_values: [] + } satisfies APIAutoPopulatedSelectMenuComponent; + }) + }; +} + +function isSelectMenuComponent(component: APIComponentInMessageActionRow): component is APISelectMenuComponent { + return component.type !== ComponentType.Button; } async function createTicket( From 1681b4249fcf90ba7974b7c4bfd9335326780a30 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:01:03 +0200 Subject: [PATCH 48/67] fix(telemetry): reduce telemetry send interval from 5 minutes to 1 minute and 30 seconds Helps us avoiingd the idle timeout from Cloudflare --- src/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telemetry.ts b/src/telemetry.ts index 58168c19..a8801782 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -23,7 +23,7 @@ const TELEMETRY_SOCKET_URL = "wss://telemetry.ticket.pm"; // const TELEMETRY_SOCKET_URL = "ws://localhost:45263"; const TELEMETRY_PROTOCOL_VERSION = "v1"; const TELEMETRY_SOCKET_PROTOCOL = `ticket.pm.telemetry.${TELEMETRY_PROTOCOL_VERSION}`; -const TELEMETRY_SEND_INTERVAL_MS = 300_000; // 5 minutes +const TELEMETRY_SEND_INTERVAL_MS = 90_000; // 1 minute and 30 seconds. const TELEMETRY_RECONNECT_MAX_DELAY_MS = 10_000; const TELEMETRY_NOTICE_KEY = "telemetryPrivacyNoticeShown"; From fb9074996f05c7d0f8ab98de6ea5ec42f1abb088 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:10:12 +0200 Subject: [PATCH 49/67] build: replace custom build scripts with tsc and resolve-tspaths * Remove custom build.mjs and run-entry.mjs scripts * Use tsc and resolve-tspaths for the build process * Update bun.lockb and package.json accordingly --- bun.lock | 152 +++++++++++++++++++++++++++--------------- package.json | 16 ++--- scripts/build.mjs | 143 --------------------------------------- scripts/run-entry.mjs | 86 ------------------------ 4 files changed, 105 insertions(+), 292 deletions(-) delete mode 100644 scripts/build.mjs delete mode 100644 scripts/run-entry.mjs diff --git a/bun.lock b/bun.lock index eaef5889..1501d215 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ "devDependencies": { "@biomejs/biome": "^2.4.12", "@types/node": "^24.9.1", - "tsx": "^4.21.0", + "resolve-tspaths": "^0.8.23", "typescript": "^6.0.2", }, }, @@ -60,57 +60,57 @@ "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@libsql/client": ["@libsql/client@0.17.2", "", { "dependencies": { "@libsql/core": "^0.17.2", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q=="], @@ -140,6 +140,12 @@ "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], @@ -154,10 +160,16 @@ "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], @@ -172,16 +184,30 @@ "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], - "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], @@ -190,18 +216,34 @@ "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "resolve-tspaths": ["resolve-tspaths@0.8.23", "", { "dependencies": { "ansi-colors": "4.1.3", "commander": "12.1.0", "fast-glob": "3.3.2" }, "peerDependencies": { "typescript": ">=3.0.3" }, "bin": { "resolve-tspaths": "dist/main.js" } }, "sha512-VMZPjXnYLHnNHXOmJ9Unkkls08zDc+0LSBUo8Rp+SKzRt8rfD9dMpBudQJ5PNG8Szex/fnwdNKzd7rqipIH/zg=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -242,7 +284,7 @@ "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], @@ -292,56 +334,56 @@ "bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], - "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], - "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], - "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], - "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], - "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], - "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], - "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], - "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], - "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], - "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], - "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], - "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], - "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], - "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], - "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], - "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], - "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], - "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], - "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], - "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], - "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], - "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], - "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], - "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], - "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], } } diff --git a/package.json b/package.json index 791082e3..08f8364e 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,12 @@ "format": "biome format .", "format:fix": "biome format --write", "drizzle:push": "drizzle-kit push", - "build": "bun scripts/build.mjs || node scripts/build.mjs", - "deploy:commands": "bun scripts/run-entry.mjs src/deploy-commands-cli.js || node scripts/run-entry.mjs src/deploy-commands-cli.js", - "migrate:v3": "bun scripts/run-entry.mjs src/migrate-v3-db.js", - "migrate:v3:config": "bun scripts/run-entry.mjs src/migrate-v3-config.js", - "migrate:v3:test": "bun scripts/verify-migrate-v3.mjs", - "start": "bun scripts/run-entry.mjs src/index.js || node scripts/run-entry.mjs src/index.js" + "build": "tsc -p tsconfig.build.json && resolve-tspaths -p tsconfig.build.json", + "start": "bun src/index.ts", + "node:start": "node dist/src/index.js", + "migrate:v3": "bun src/migrate-v3-db.ts", + "migrate:v3:config": "bun src/migrate-v3-config.ts", + "migrate:v3:test": "bun scripts/verify-migrate-v3.mjs" }, "dependencies": { "@discordjs/core": "2.4.0", @@ -40,7 +40,7 @@ "devDependencies": { "@biomejs/biome": "^2.4.12", "@types/node": "^24.9.1", - "typescript": "^6.0.2", - "tsx": "^4.21.0" + "resolve-tspaths": "^0.8.23", + "typescript": "^6.0.2" } } diff --git a/scripts/build.mjs b/scripts/build.mjs deleted file mode 100644 index acabc743..00000000 --- a/scripts/build.mjs +++ /dev/null @@ -1,143 +0,0 @@ -import { spawn } from "node:child_process"; -import { readdir, readFile, rm, writeFile } from "node:fs/promises"; -import path from "node:path"; -import process from "node:process"; -import { fileURLToPath } from "node:url"; -import ts from "typescript"; - -const rootDirectory = fileURLToPath(new URL("..", import.meta.url)); -const distDirectory = path.join(rootDirectory, "dist"); -const tsconfigPath = path.join(rootDirectory, "tsconfig.build.json"); -const tscEntrypoint = path.join(rootDirectory, "node_modules", "typescript", "bin", "tsc"); - -await rm(distDirectory, { recursive: true, force: true }); -await run(process.execPath, [tscEntrypoint, "--project", tsconfigPath]); -await rewriteDirectory(distDirectory); - -async function rewriteDirectory(directory) { - const entries = await readdir(directory, { withFileTypes: true }); - - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); - - if (entry.isDirectory()) { - await rewriteDirectory(entryPath); - continue; - } - - if (!entry.isFile() || path.extname(entry.name) !== ".js") { - continue; - } - - const source = await readFile(entryPath, "utf8"); - const rewritten = rewriteImports(source, entryPath); - - if (rewritten !== source) { - await writeFile(entryPath, rewritten); - } - } -} - -function rewriteImports(source, filePath) { - const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); - const edits = []; - - const visit = (node) => { - if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) { - edits.push({ - start: node.moduleSpecifier.getStart(sourceFile) + 1, - end: node.moduleSpecifier.getEnd() - 1, - value: rewriteSpecifier(node.moduleSpecifier.text, filePath) - }); - } - - if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [argument] = node.arguments; - - if (argument && ts.isStringLiteralLike(argument)) { - edits.push({ - start: argument.getStart(sourceFile) + 1, - end: argument.getEnd() - 1, - value: rewriteSpecifier(argument.text, filePath) - }); - } - } - - ts.forEachChild(node, visit); - }; - - visit(sourceFile); - - return edits - .sort((left, right) => right.start - left.start) - .reduce((currentSource, edit) => { - return `${currentSource.slice(0, edit.start)}${edit.value}${currentSource.slice(edit.end)}`; - }, source); -} - -function rewriteSpecifier(specifier, filePath) { - if (specifier.startsWith("@/")) { - const targetPath = path.join(rootDirectory, "dist", "src", specifier.slice(2)); - return toImportPath(path.relative(path.dirname(filePath), ensureJsExtension(targetPath))); - } - - if (specifier.startsWith(".")) { - return normalizeRelativeExtension(specifier); - } - - return specifier; -} - -function normalizeRelativeExtension(specifier) { - if (/\.(?:c|m)?js$|\.json$|\.node$/u.test(specifier)) { - return specifier; - } - - if (/\.(?:cts|mts|tsx|ts)$/u.test(specifier)) { - return specifier.replace(/\.(?:cts|mts|tsx|ts)$/u, ".js"); - } - - return `${specifier}.js`; -} - -function ensureJsExtension(filePath) { - if (/\.(?:c|m)?js$/u.test(filePath)) { - return filePath; - } - - if (/\.(?:cts|mts|tsx|ts)$/u.test(filePath)) { - return filePath.replace(/\.(?:cts|mts|tsx|ts)$/u, ".js"); - } - - return `${filePath}.js`; -} - -function toImportPath(relativePath) { - const normalizedPath = relativePath.split(path.sep).join("/"); - - if (normalizedPath.startsWith(".")) { - return normalizedPath; - } - - return `./${normalizedPath}`; -} - -function run(command, args) { - return new Promise((resolvePromise, rejectPromise) => { - const child = spawn(command, args, { - cwd: rootDirectory, - env: process.env, - stdio: "inherit" - }); - - child.on("error", rejectPromise); - child.on("exit", (code) => { - if (code === 0) { - resolvePromise(); - return; - } - - rejectPromise(new Error(`Command failed with exit code ${code ?? "unknown"}.`)); - }); - }); -} diff --git a/scripts/run-entry.mjs b/scripts/run-entry.mjs deleted file mode 100644 index f657d7b9..00000000 --- a/scripts/run-entry.mjs +++ /dev/null @@ -1,86 +0,0 @@ -import { spawn } from "node:child_process"; -import { readdir, stat } from "node:fs/promises"; -import path from "node:path"; -import process from "node:process"; -import { fileURLToPath } from "node:url"; - -const rootDirectory = fileURLToPath(new URL("..", import.meta.url)); -const entryArgument = process.argv[2]; -const entryArgs = process.argv.slice(3); - -if (!entryArgument) { - throw new Error("Missing dist entry argument."); -} - -const distEntry = path.join(rootDirectory, "dist", entryArgument); - -if (await shouldBuild(distEntry)) { - await run(process.execPath, [path.join(rootDirectory, "scripts", "build.mjs")]); -} - -await run(process.execPath, [distEntry, ...entryArgs]); - -async function shouldBuild(distEntryPath) { - let distEntryStat; - - try { - distEntryStat = await stat(distEntryPath); - } catch { - return true; - } - - const latestSourceMtime = await getLatestSourceMtime([ - path.join(rootDirectory, "src"), - path.join(rootDirectory, "config"), - path.join(rootDirectory, "i18n"), - path.join(rootDirectory, "messages"), - path.join(rootDirectory, "drizzle.config.ts") - ]); - - return latestSourceMtime > distEntryStat.mtimeMs; -} - -async function getLatestSourceMtime(paths) { - let latest = 0; - - for (const sourcePath of paths) { - const sourceStat = await stat(sourcePath).catch(() => null); - - if (!sourceStat) { - continue; - } - - if (sourceStat.isDirectory()) { - const entries = await readdir(sourcePath, { withFileTypes: true }); - const childPaths = entries.map((entry) => path.join(sourcePath, entry.name)); - latest = Math.max(latest, await getLatestSourceMtime(childPaths)); - continue; - } - - if (sourceStat.isFile() && /\.(?:ts|mts|cts|js|mjs|json)$/u.test(sourcePath)) { - latest = Math.max(latest, sourceStat.mtimeMs); - } - } - - return latest; -} - -function run(command, args) { - return new Promise((resolvePromise, rejectPromise) => { - const child = spawn(command, args, { - cwd: rootDirectory, - env: process.env, - stdio: "inherit" - }); - - child.on("error", rejectPromise); - child.on("exit", (code) => { - if (code === 0) { - resolvePromise(); - return; - } - - rejectPromise(new Error(`Command failed with exit code ${code ?? "unknown"}.`)); - }); - }); -} From 1a8e673ba4a6c18dc7d0cfa567b8d25722cab078 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:10:38 +0200 Subject: [PATCH 50/67] refactor: remove unused deploy-commands-cli script * This script is no longer used by the build or deployment process --- src/deploy-commands-cli.ts | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 src/deploy-commands-cli.ts diff --git a/src/deploy-commands-cli.ts b/src/deploy-commands-cli.ts deleted file mode 100644 index 32fdf92a..00000000 --- a/src/deploy-commands-cli.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* -Ticket-Bot is licensed under the GNU Affero General Public License, -version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. - -Additional Term under GNU AGPL v3, Section 7(b): - -You are required to preserve and display, in a location clearly visible -to end users interacting with the bot (such as bot embeds, the bot's -"Bio" Discord profile, status, or equivalent), a notice that the -software is powered by Ticket-Bot, including a link to the original -project repository or to its website. - -This notice must not be removed, obscured, or replaced. -*/ - -import { deployCommands } from "@/deploy-commands"; - -deployCommands().catch((error) => { - console.error("[deploy] Failed to deploy commands", error); - process.exit(1); -}); - -/* -Ticket-Bot is licensed under the GNU Affero General Public License, -version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. - -Additional Term under GNU AGPL v3, Section 7(b): - -You are required to preserve and display, in a location clearly visible -to end users interacting with the bot (such as bot embeds, the bot's -"Bio" Discord profile, status, or equivalent), a notice that the -software is powered by Ticket-Bot, including a link to the original -project repository or to its website. - -This notice must not be removed, obscured, or replaced. -*/ From e71e98fb155b690fdca6fa961b8ec2f26a818b22 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:52:20 +0200 Subject: [PATCH 51/67] ci(github): migrate workflows and dependabot to bun * Update dependabot package-ecosystem to bun * Replace setup-node with setup-bun in builder workflow * Replace npm commands with bun equivalents * Add new verification steps (typecheck, i18n, lint, format, drizzle check) --- .github/dependabot.yml | 2 +- .github/workflows/builder.yml | 48 +++++++++++++++++------------------ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3a3cce57..fd8ee2b9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values + - package-ecosystem: "bun" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 3881a7fb..3932929e 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -11,39 +11,37 @@ permissions: contents: read jobs: - main: + checks: runs-on: ubuntu-latest - strategy: - matrix: - os: [ubuntu-latest] - node-version: [18] - steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 with: persist-credentials: false - - uses: actions/setup-node@v3 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 with: - node-version: ${{ matrix.node-version }} + bun-version: latest - - name: Cache node_modules - id: cache-node_modules - uses: actions/cache@v3 - with: - path: node_modules - key: node_modules-${{ matrix.os }}-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: i18n + run: bun run i18n + + - name: Lint + run: bun run lint - - name: npm i - if: steps.cache-node_modules.outputs.cache-hit != 'true' || github.event_name == 'pull_request' - run: npm i --no-audit --force + - name: Format + run: bun run format - - name: ESLint - run: npm run lint:check + - name: Drizzle + run: bunx drizzle-kit check - - name: build - run: npm run build --if-present - - - name: Prisma - run: npx prisma generate + - name: Build + run: bun run build From 885a36fbb3b6adf3b9e8e61efe28f4cd6d645b4d Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:15:35 +0200 Subject: [PATCH 52/67] refactor: type interaction response bodies --- src/core/respond.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/core/respond.ts b/src/core/respond.ts index eba244c4..92a5d174 100644 --- a/src/core/respond.ts +++ b/src/core/respond.ts @@ -17,7 +17,14 @@ import type { APIApplicationCommandAutocompleteInteraction, APIChatInputApplicationCommandInteraction, APIMessageComponentInteraction, - APIModalSubmitInteraction + APIModalSubmitInteraction, + CreateAutocompleteResponseOptions, + CreateInteractionDeferResponseOptions, + CreateInteractionFollowUpResponseOptions, + CreateInteractionResponseOptions, + CreateInteractionUpdateMessageResponseOptions, + CreateModalResponseOptions, + EditInteractionResponseOptions } from "@discordjs/core"; import { MessageFlags } from "@discordjs/core"; import type { BotApp } from "@/core/types"; @@ -29,31 +36,39 @@ type ReplyableInteraction = type ModalCapableInteraction = APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction; -export async function reply(app: BotApp, interaction: ReplyableInteraction, body: any) { +export async function reply(app: BotApp, interaction: ReplyableInteraction, body: CreateInteractionResponseOptions) { return app.client.api.interactions.reply(interaction.id, interaction.token, body); } -export async function deferReply(app: BotApp, interaction: ReplyableInteraction, body?: any) { +export async function deferReply(app: BotApp, interaction: ReplyableInteraction, body?: CreateInteractionDeferResponseOptions) { return app.client.api.interactions.defer(interaction.id, interaction.token, body); } -export async function editReply(app: BotApp, interaction: ReplyableInteraction, body: any) { +export async function editReply(app: BotApp, interaction: ReplyableInteraction, body: EditInteractionResponseOptions) { return app.client.api.interactions.editReply(app.applicationId, interaction.token, body); } -export async function followUp(app: BotApp, interaction: ReplyableInteraction, body: any) { +export async function followUp(app: BotApp, interaction: ReplyableInteraction, body: CreateInteractionFollowUpResponseOptions) { return app.client.api.interactions.followUp(app.applicationId, interaction.token, body); } -export async function updateMessage(app: BotApp, interaction: APIMessageComponentInteraction, body: any) { +export async function updateMessage( + app: BotApp, + interaction: APIMessageComponentInteraction, + body: CreateInteractionUpdateMessageResponseOptions +) { return app.client.api.interactions.updateMessage(interaction.id, interaction.token, body); } -export async function showModal(app: BotApp, interaction: ModalCapableInteraction, body: any) { +export async function showModal(app: BotApp, interaction: ModalCapableInteraction, body: CreateModalResponseOptions) { return app.client.api.interactions.createModal(interaction.id, interaction.token, body); } -export async function replyWithAutocomplete(app: BotApp, interaction: APIApplicationCommandAutocompleteInteraction, body: any) { +export async function replyWithAutocomplete( + app: BotApp, + interaction: APIApplicationCommandAutocompleteInteraction, + body: CreateAutocompleteResponseOptions +) { return app.client.api.interactions.createAutocompleteResponse(interaction.id, interaction.token, body); } From d66488ddf71edf6be559dea2b4567bb64cac8ff8 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:16:13 +0200 Subject: [PATCH 53/67] refactor: type message template slots --- src/features/tickets/messages.ts | 41 +++++++++++++------------ src/features/tickets/ticket-workflow.ts | 5 +-- src/features/tickets/types.ts | 22 ++++++++++++- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/features/tickets/messages.ts b/src/features/tickets/messages.ts index 9947c8c8..536fc748 100644 --- a/src/features/tickets/messages.ts +++ b/src/features/tickets/messages.ts @@ -19,7 +19,13 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import type { APIButtonComponentWithCustomId, APIMessageTopLevelComponent } from "@discordjs/core"; import { ComponentType, MessageFlags } from "@discordjs/core"; import { MESSAGE_TEMPLATES_DIRECTORY } from "@/features/tickets/constants"; -import type { LoadedMessageTemplate, MessageTemplateSource } from "@/features/tickets/types"; +import type { + DiscordMessageTemplate, + LoadedMessageTemplate, + MessageTemplateComponent, + MessageTemplateSlotComponent, + MessageTemplateSource +} from "@/features/tickets/types"; import { renderTemplate } from "@/features/tickets/utils"; import type { BotApp } from "@/core/types"; @@ -35,7 +41,7 @@ const COMPONENTS_V2_TYPES = new Set([ ComponentType.Container ]); -export function createMessageSlot(slot: string): any { +export function createMessageSlot(slot: string): MessageTemplateSlotComponent { return { type: TEMPLATE_SLOT_TYPE, slot, @@ -73,7 +79,7 @@ export async function loadMessageTemplate( return applyComponentsV2Defaults(renderedPayload); } -export function finalizeMessageTemplate(payload: LoadedMessageTemplate) { +export function finalizeMessageTemplate(payload: LoadedMessageTemplate): DiscordMessageTemplate { return sanitizeMessageTemplate(applyComponentsV2Defaults(payload)); } @@ -297,9 +303,9 @@ function applyComponentsV2Defaults(payload: LoadedMessageTemplate): LoadedMessag }; } -function sanitizeMessageTemplate(payload: LoadedMessageTemplate): LoadedMessageTemplate { +function sanitizeMessageTemplate(payload: LoadedMessageTemplate): DiscordMessageTemplate { const usesV2 = usesComponentsV2(payload); - const nextPayload: LoadedMessageTemplate = {}; + const nextPayload: DiscordMessageTemplate = {}; if (payload.allowed_mentions) { nextPayload.allowed_mentions = payload.allowed_mentions; @@ -326,17 +332,17 @@ function sanitizeMessageTemplate(payload: LoadedMessageTemplate): LoadedMessageT return nextPayload; } -function hasComponentsV2Components(components: APIMessageTopLevelComponent[] | undefined) { - return components?.some((component) => COMPONENTS_V2_TYPES.has(component.type)) ?? false; +function hasComponentsV2Components(components: MessageTemplateComponent[] | undefined) { + return components?.some((component) => typeof component.type === "number" && COMPONENTS_V2_TYPES.has(component.type)) ?? false; } function injectManyIntoSlots( - components: APIMessageTopLevelComponent[], + components: MessageTemplateComponent[], injectedComponents: APIMessageTopLevelComponent[], slot: string ): { replaced: boolean; - value: APIMessageTopLevelComponent[]; + value: MessageTemplateComponent[]; } { let replaced = false; @@ -366,16 +372,16 @@ function injectManyIntoSlots( return { replaced, - value: visit(components) as APIMessageTopLevelComponent[] + value: visit(components) as MessageTemplateComponent[] }; } function appendComponentsToFirstContainer( - components: APIMessageTopLevelComponent[], + components: MessageTemplateComponent[], appendedComponents: APIMessageTopLevelComponent[] ): { replaced: boolean; - value: APIMessageTopLevelComponent[]; + value: MessageTemplateComponent[]; } { let replaced = false; @@ -407,13 +413,13 @@ function appendComponentsToFirstContainer( return { replaced, - value: visit(components) as APIMessageTopLevelComponent[] + value: visit(components) as MessageTemplateComponent[] }; } -function stripTemplateSlots(components: APIMessageTopLevelComponent[] | undefined) { +function stripTemplateSlots(components: MessageTemplateComponent[] | undefined): APIMessageTopLevelComponent[] | undefined { if (!components?.length) { - return components; + return undefined; } const visit = (value: unknown): unknown => { @@ -444,10 +450,7 @@ function stripTemplateSlots(components: APIMessageTopLevelComponent[] | undefine return visit(components) as APIMessageTopLevelComponent[]; } -function isTemplateSlot(value: unknown): value is { - slot: string; - slot_kind?: string; -} { +function isTemplateSlot(value: unknown): value is MessageTemplateSlotComponent { return Boolean( value && typeof value === "object" && diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index bd512735..edf92df3 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -23,7 +23,8 @@ import type { APIModalSubmitInteraction, APIModalSubmitTextInputComponent, APISelectMenuComponent, - APIStringSelectComponent + APIStringSelectComponent, + APITextInputComponent } from "@discordjs/core"; import { ButtonStyle, @@ -577,7 +578,7 @@ type WelcomeTemplateContainer = Omit & { }; type TicketActionRows = APIActionRowComponent[]; -function createQuestionInput(question: TicketQuestionConfig) { +function createQuestionInput(question: TicketQuestionConfig): APITextInputComponent { return { type: ComponentType.TextInput, custom_id: question.key, diff --git a/src/features/tickets/types.ts b/src/features/tickets/types.ts index b582ef0d..ee75ada9 100644 --- a/src/features/tickets/types.ts +++ b/src/features/tickets/types.ts @@ -14,6 +14,7 @@ This notice must not be removed, obscured, or replaced. */ import type { APIAllowedMentions, APIEmbed, APIMessageTopLevelComponent } from "@discordjs/core"; +import type { APIComponentInContainer, APIContainerComponent } from "discord-api-types/v10"; import type { Locales, TranslationFunctions } from "../../../i18n/i18n-types.js"; import type { VersionedConfig } from "@/config/index"; @@ -29,15 +30,34 @@ export type TicketClaimMode = TicketClaimsConfig["mode"]; export type LogsConfig = CurrentConfig["logs"]; export type LogEventToggleKey = keyof NonNullable; +export interface MessageTemplateSlotComponent { + type: "template-slot"; + slot: string; + slot_kind?: "many"; +} + +export type MessageTemplateContainerChild = APIComponentInContainer | MessageTemplateSlotComponent; +export type MessageTemplateContainerComponent = Omit & { + components: MessageTemplateContainerChild[]; +}; +export type MessageTemplateComponent = + | Exclude + | MessageTemplateContainerComponent + | MessageTemplateSlotComponent; + export interface LoadedMessageTemplate { allowed_mentions?: APIAllowedMentions; content?: string; embeds?: APIEmbed[]; - components?: APIMessageTopLevelComponent[]; + components?: MessageTemplateComponent[]; flags?: number; useComponentsV2?: boolean; } +export type DiscordMessageTemplate = Omit & { + components?: APIMessageTopLevelComponent[]; +}; + export interface MessageTemplateContext { locale: Locales; LL: TranslationFunctions; From 910b9e918fbbe53c3feae1f52544cfc4c8ebd989 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:16:40 +0200 Subject: [PATCH 54/67] refactor: remove redundant fetch casts --- src/events/ready.ts | 2 +- src/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/events/ready.ts b/src/events/ready.ts index 2809e457..f8eaa70a 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -169,7 +169,7 @@ async function announceStartup(app: BotApp, tag: string, userId: string) { async function fetchSponsors() { try { - const response = (await fetch(SPONSORS_URL)) as any; + const response = await fetch(SPONSORS_URL); if (!response.ok) { return []; diff --git a/src/index.ts b/src/index.ts index efdd4718..088f30ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,11 +55,11 @@ main().catch(async (error) => { async function checkForUpdates() { try { - const response = (await fetch(REPOSITORY_TAGS_URL, { + const response = await fetch(REPOSITORY_TAGS_URL, { headers: { accept: "application/vnd.github+json" } - })) as any; + }); if (!response.ok) { logger.warn(`Failed to pull latest version from server (${response.status}).`); From d30378b7881b4c040d0ba2e727504294c54722c6 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:21:20 +0200 Subject: [PATCH 55/67] refactor: reuse panel opener helper --- src/features/tickets/panel-sync.ts | 62 +----------------------------- 1 file changed, 2 insertions(+), 60 deletions(-) diff --git a/src/features/tickets/panel-sync.ts b/src/features/tickets/panel-sync.ts index e6dac1b9..d1df1018 100644 --- a/src/features/tickets/panel-sync.ts +++ b/src/features/tickets/panel-sync.ts @@ -33,7 +33,7 @@ import { userCanAccessTicketType, validatePanelConfig } from "@/features/tickets/config-access"; -import { appendMessageText, finalizeMessageTemplate, loadMessageTemplate } from "@/features/tickets/messages"; +import { appendMessageText, appendPanelOpener, finalizeMessageTemplate, loadMessageTemplate } from "@/features/tickets/messages"; import { continueTicketOpen } from "@/features/tickets/ticket-workflow"; import type { ButtonPanelEntryConfig, PanelConfig, PanelOpenerConfig } from "@/features/tickets/types"; import { chunk, getMemberRoleIds, mapButtonStyle, toPartialEmoji } from "@/features/tickets/utils"; @@ -165,7 +165,7 @@ async function recreatePanelMessage( async function buildPanelMessage(app: BotApp, panelKey: string, panel: PanelConfig) { const messageTemplate = await loadMessageTemplate(app, panel.message); const withConfiguredText = appendMessageText(messageTemplate, panel.content); - const body = placePanelOpener(withConfiguredText, buildPanelComponents(app, panelKey, panel)); + const body = appendPanelOpener(withConfiguredText, buildPanelComponents(app, panelKey, panel)); return finalizeMessageTemplate({ ...body, @@ -176,64 +176,6 @@ async function buildPanelMessage(app: BotApp, panelKey: string, panel: PanelConf }); } -function placePanelOpener( - payload: Awaited>, - openerComponents: APIMessageTopLevelComponent[] -) { - if (!openerComponents.length) { - return payload; - } - - let replacedSlot = false; - let appendedToContainer = false; - - const visit = (value: unknown): unknown => { - if (Array.isArray(value)) { - return value.flatMap((entry) => { - if (isPanelOpenerSlot(entry)) { - replacedSlot = true; - return structuredClone(openerComponents); - } - - const nextEntry = visit(entry); - return Array.isArray(nextEntry) ? nextEntry : [nextEntry]; - }); - } - - if (!value || typeof value !== "object") { - return value; - } - - if ( - !replacedSlot && - !appendedToContainer && - "type" in value && - value.type === ComponentType.Container && - "components" in value && - Array.isArray(value.components) - ) { - appendedToContainer = true; - return { - ...value, - components: [...value.components, ...structuredClone(openerComponents)] - }; - } - - return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry)])); - }; - - return { - ...payload, - components: visit(payload.components ?? []) as APIMessageTopLevelComponent[] - }; -} - -function isPanelOpenerSlot(value: unknown): value is { - slot: string; -} { - return Boolean(value && typeof value === "object" && "slot" in value && value.slot === "panel-opener"); -} - function buildPanelComponents(app: BotApp, panelKey: string, panel: PanelConfig): APIMessageTopLevelComponent[] { switch (panel.opener.type) { case "inline-select": From 2e31393fc984dd9a3ee96cfc198fc281db84294b Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:21:55 +0200 Subject: [PATCH 56/67] refactor: extract modal text parsing --- src/features/tickets/close-workflow.ts | 16 ++----------- src/features/tickets/ticket-workflow.ts | 31 +++++++------------------ src/features/tickets/utils.ts | 24 +++++++++++++++++-- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index 6dbc9bfd..289ded1a 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -41,7 +41,7 @@ import { getInvitedUserIds, revokeTicketParticipantAccess } from "@/features/tic import { findTicketByChannel, getOpenTicketByChannel } from "@/features/tickets/records"; import { formatClaimStatus, formatTranscriptStatus, getDefaultNoReason } from "@/features/tickets/text"; import { startTranscriptJob } from "@/features/tickets/transcripts"; -import { getInteractionUser, getMemberRoleIds } from "@/features/tickets/utils"; +import { getInteractionUser, getMemberRoleIds, getModalTextInputValues } from "@/features/tickets/utils"; const DEFAULT_CLOSE_DM_MESSAGE = "tickets/ticket-closed-dm"; const DEFAULT_CLOSE_CHANNEL_MESSAGE = "tickets/ticket-closed"; @@ -613,19 +613,7 @@ function createCloseStatusUpdater( } function readCloseReason(interaction: APIModalSubmitInteraction) { - for (const component of interaction.data.components) { - if (!("components" in component)) { - continue; - } - - for (const child of component.components) { - if (child.type === ComponentType.TextInput && child.custom_id === "reason" && "value" in child) { - return child.value.trim() || null; - } - } - } - - return null; + return getModalTextInputValues(interaction).get("reason")?.trim() || null; } function normalizeCloseReason(app: BotApp, reason: string | null) { diff --git a/src/features/tickets/ticket-workflow.ts b/src/features/tickets/ticket-workflow.ts index edf92df3..ce763d4a 100644 --- a/src/features/tickets/ticket-workflow.ts +++ b/src/features/tickets/ticket-workflow.ts @@ -21,7 +21,6 @@ import type { APIMessageComponentInteraction, APIMessageTopLevelComponent, APIModalSubmitInteraction, - APIModalSubmitTextInputComponent, APISelectMenuComponent, APIStringSelectComponent, APITextInputComponent @@ -64,7 +63,13 @@ import type { TicketRenderTokens, TicketTypeConfig } from "@/features/tickets/types"; -import { getInteractionUser, getMemberRoleIds, renderChannelName, renderTemplate } from "@/features/tickets/utils"; +import { + getInteractionUser, + getMemberRoleIds, + getModalTextInputValues, + renderChannelName, + renderTemplate +} from "@/features/tickets/utils"; interface TicketOpenReasonData { answers: string[]; @@ -83,7 +88,7 @@ export async function handleOpenFormSubmit(context: ComponentExecutionContext, i const ticketType = getTicketType(context.app, ticketTypeKey); const questions = ticketType.openForm?.questions ?? []; - const answers = extractSubmittedValues(interaction); + const answers = getModalTextInputValues(interaction); const reason = questions.length > 0 ? createTicketOpenReason(context.app, questions, answers) : createDefaultTicketOpenReason(context.app); @@ -605,26 +610,6 @@ async function getNextTicketNumber(app: BotApp) { return Number(rows[0]?.count ?? 0) + 1; } -function extractSubmittedValues(interaction: APIModalSubmitInteraction) { - const values = new Map(); - - for (const component of interaction.data.components) { - if (!("components" in component)) { - continue; - } - - for (const child of component.components) { - if (child.type !== ComponentType.TextInput) { - continue; - } - - values.set(child.custom_id, (child as APIModalSubmitTextInputComponent).value); - } - } - - return values; -} - function createDefaultTicketOpenReason(app: BotApp): TicketOpenReasonData { return { answers: [], diff --git a/src/features/tickets/utils.ts b/src/features/tickets/utils.ts index 6e3563e6..a004427b 100644 --- a/src/features/tickets/utils.ts +++ b/src/features/tickets/utils.ts @@ -13,8 +13,8 @@ project repository or to its website. This notice must not be removed, obscured, or replaced. */ -import type { APIUser } from "@discordjs/core"; -import { ButtonStyle } from "@discordjs/core"; +import type { APIModalSubmitInteraction, APIModalSubmitTextInputComponent, APIUser } from "@discordjs/core"; +import { ButtonStyle, ComponentType } from "@discordjs/core"; import type { ButtonStyleName } from "@/features/tickets/types"; export function renderTemplate(template: string, tokens: Record) { @@ -101,6 +101,26 @@ export function getMemberRoleIds(interaction: { member?: { roles?: string[] } | return Array.isArray(interaction.member?.roles) ? interaction.member.roles : []; } +export function getModalTextInputValues(interaction: APIModalSubmitInteraction) { + const values = new Map(); + + for (const component of interaction.data.components) { + if (!("components" in component)) { + continue; + } + + for (const child of component.components) { + if (child.type !== ComponentType.TextInput) { + continue; + } + + values.set(child.custom_id, (child as APIModalSubmitTextInputComponent).value); + } + } + + return values; +} + /* Ticket-Bot is licensed under the GNU Affero General Public License, version 3 only ("AGPL-3.0-only"). See LICENSE.md for the full license text. From 169d5c585f60c7072838a0653cdc82a6a97f2d7a Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:22:26 +0200 Subject: [PATCH 57/67] refactor: use direct BotApp type --- src/features/commands/mass_add/command.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/commands/mass_add/command.ts b/src/features/commands/mass_add/command.ts index 36c5daf0..a48a25c3 100644 --- a/src/features/commands/mass_add/command.ts +++ b/src/features/commands/mass_add/command.ts @@ -17,6 +17,7 @@ import { MessageFlags } from "@discordjs/core"; import { ApplicationCommandOptionType } from "discord-api-types/v10"; import { defineCommand } from "@/core/defineCommand"; import { reply } from "@/core/respond"; +import type { BotApp } from "@/core/types"; import { getStringOption } from "@/features/commands/shared/options"; import { sendTicketLog } from "@/features/logs/service"; import { createTicketLogContext } from "@/features/logs/utils"; @@ -138,7 +139,7 @@ function parseRequestedUserIds(rawValue: string) { } function buildMassAddSummary( - app: Parameters[0], + app: BotApp, addedUserIds: string[], skippedUserIds: string[], invalidUserIds: string[], From 19b6f5eec0c44144454e7bcded0321abe0bcaf1e Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:26:08 +0200 Subject: [PATCH 58/67] refactor: build BotApp with object literal --- src/app.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/app.ts b/src/app.ts index d79e916b..7b6f5b41 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,18 +44,22 @@ export async function createBotApp() { ]); const i18n = createBotI18n(botConfig.lang, logger); const registry = createHandlerRegistry({ commands, features, events, logger, LL: i18n.LL }); + let router: InteractionRouter; + const app: BotApp = { + client, + db, + config: botConfig, + logger, + applicationId: botConfig.clientId, + locale: i18n.locale, + LL: i18n.LL, + registry, + get router() { + return router; + } + }; - const app = {} as BotApp; - app.client = client; - app.db = db; - app.config = botConfig; - app.logger = logger; - app.applicationId = botConfig.clientId; - app.locale = i18n.locale; - app.LL = i18n.LL; - app.registry = registry; - - app.router = new InteractionRouter(app); + router = new InteractionRouter(app); registerEvents(app); From 94cbba214d067a46e15c8d0ac65548d03b5cfd2d Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 1 May 2026 13:38:22 +0200 Subject: [PATCH 59/67] chore: update dependencies to latest versions --- bun.lock | 68 ++++++++++++++++++---------------------------------- package.json | 12 +++++----- 2 files changed, 29 insertions(+), 51 deletions(-) diff --git a/bun.lock b/bun.lock index 1501d215..8824e786 100644 --- a/bun.lock +++ b/bun.lock @@ -7,9 +7,9 @@ "@discordjs/core": "2.4.0", "@discordjs/rest": "^2.6.1", "@discordjs/ws": "^2.0.4", - "@libsql/client": "^0.17.2", - "@ticketpm/core": "^0.0.7", - "@ticketpm/discord-api": "^0.0.7", + "@libsql/client": "^0.17.3", + "@ticketpm/core": "^0.0.11", + "@ticketpm/discord-api": "^0.0.11", "discord-api-types": "^0.38.47", "dotenv": "^17.4.2", "drizzle-kit": "^0.31.10", @@ -18,31 +18,31 @@ "typesafe-i18n": "^5.27.1", }, "devDependencies": { - "@biomejs/biome": "^2.4.12", - "@types/node": "^24.9.1", + "@biomejs/biome": "^2.4.13", + "@types/node": "^25.6.0", "resolve-tspaths": "^0.8.23", - "typescript": "^6.0.2", + "typescript": "^6.0.3", }, }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.12", "@biomejs/cli-darwin-x64": "2.4.12", "@biomejs/cli-linux-arm64": "2.4.12", "@biomejs/cli-linux-arm64-musl": "2.4.12", "@biomejs/cli-linux-x64": "2.4.12", "@biomejs/cli-linux-x64-musl": "2.4.12", "@biomejs/cli-win32-arm64": "2.4.12", "@biomejs/cli-win32-x64": "2.4.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA=="], + "@biomejs/biome": ["@biomejs/biome@2.4.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.13", "@biomejs/cli-darwin-x64": "2.4.13", "@biomejs/cli-linux-arm64": "2.4.13", "@biomejs/cli-linux-arm64-musl": "2.4.13", "@biomejs/cli-linux-x64": "2.4.13", "@biomejs/cli-linux-x64-musl": "2.4.13", "@biomejs/cli-win32-arm64": "2.4.13", "@biomejs/cli-win32-x64": "2.4.13" }, "bin": { "biome": "bin/biome" } }, "sha512-gLXOwkOBBg0tr7bDsqlkIh4uFeKuMjxvqsrb1Tukww1iDmHcfr4Uu8MoQxp0Rcte+69+osRNWXwHsu/zxT6XqA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-BKrJklbaFN4p1Ts4kPBczo+PkbsHQg57kmJ+vON9u2t6uN5okYHaSr7h/MutPCWQgg2lglaWoSmm+zhYW+oOkg=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-U5MsuBQW25dXaYtqWWSPM3P96H6Y+fHuja3TQpMNnylocHW0tEbtFTDlUj6oM+YJLntvEkQy4grBvQNUD4+RCg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.13", "", { "os": "linux", "cpu": "x64" }, "sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.13", "", { "os": "linux", "cpu": "x64" }, "sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.13", "", { "os": "win32", "cpu": "x64" }, "sha512-tTcMkXyBrmHi9BfrD2VNHs/5rYIUKETqsBlYOvSAABwBkJhSDVb5e7wPukftsQbO3WzQkXe6kaztC6WtUOXSoQ=="], "@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], @@ -112,15 +112,15 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "@libsql/client": ["@libsql/client@0.17.2", "", { "dependencies": { "@libsql/core": "^0.17.2", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q=="], + "@libsql/client": ["@libsql/client@0.17.3", "", { "dependencies": { "@libsql/core": "^0.17.3", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-HXk9wiAoJbKFbyBH4O+aEhN6ir5ERXuXvwE5OD2eR4/5RUa3Pw/8L9zrnVdU+iNJitRvisPWaIwmhkO3bH7giA=="], - "@libsql/core": ["@libsql/core@0.17.2", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-L8qv12HZ/jRBcETVR3rscP0uHNxh+K3EABSde6scCw7zfOdiLqO3MAkJaeE1WovPsjXzsN/JBoZED4+7EZVT3g=="], + "@libsql/core": ["@libsql/core@0.17.3", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-2UjK1i7JBkMduJo4WdvvBxMMvVJ31pArBZNONyz/GCJJAH+1UHat2X6vn10S/WpY5fKzIT98WqYFl2vzWRLOfg=="], "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.29", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A=="], "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.29", "", { "os": "darwin", "cpu": "x64" }, "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ=="], - "@libsql/hrana-client": ["@libsql/hrana-client@0.9.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "cross-fetch": "^4.0.0", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw=="], + "@libsql/hrana-client": ["@libsql/hrana-client@0.10.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5" } }, "sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw=="], "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], @@ -150,11 +150,11 @@ "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], - "@ticketpm/core": ["@ticketpm/core@0.0.7", "", {}, "sha512-Zda8J8Vophs7R86pG57p1bCCkZlox/cGORRpg4OqnfFy/JnjHnYf/n9hKgTbb05AF0d0zTV8qdjMPh6ABDd2Wg=="], + "@ticketpm/core": ["@ticketpm/core@0.0.11", "", {}, "sha512-VFOkwgzEEYtnMzAAR7z9hTb2ZaRfyUjb96i1JCd9EdrxuQaMZpU5MtktmeEg9wx2hs/9smS8zcw5W96xnqR9Xw=="], - "@ticketpm/discord-api": ["@ticketpm/discord-api@0.0.7", "", { "dependencies": { "@ticketpm/core": "0.0.7", "discord-api-types": "^0.37.119" } }, "sha512-FP2lbMik2+l8e+j6e22AxyHULoeXUkecud2o73ajqIUBvdunlUZblx3QwaAoLWLfAKJdrPjP4cTYcuJIzYXXWQ=="], + "@ticketpm/discord-api": ["@ticketpm/discord-api@0.0.11", "", { "dependencies": { "@ticketpm/core": "0.0.11", "discord-api-types": "^0.37.119" } }, "sha512-BTz5UHhYV+u4ZAPQnjyTHghwOXYxUkImhSKFlVlgu4/84eqAmQDgDvRKIZm+Mrx+a/rz0850dVBXAt0Ziz2Njw=="], - "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -170,10 +170,6 @@ "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], - - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "discord-api-types": ["discord-api-types@0.38.47", "", {}, "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA=="], @@ -190,12 +186,8 @@ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], @@ -220,10 +212,6 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], @@ -244,25 +232,17 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "typesafe-i18n": ["typesafe-i18n@5.27.1", "", { "peerDependencies": { "typescript": ">=3.5.1" }, "bin": { "typesafe-i18n": "cli/typesafe-i18n.mjs" } }, "sha512-749uWo2ZXETT//kWjVYPm8QPYR8xLh8G0wLfoAyCAtAmysX67uCaAyLjAjAWojL6fuJpE5B6yIjwvO9orXzUPg=="], - "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], @@ -282,8 +262,6 @@ "bun-types/@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], - "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], diff --git a/package.json b/package.json index 08f8364e..f0bb022a 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ "@discordjs/core": "2.4.0", "@discordjs/rest": "^2.6.1", "@discordjs/ws": "^2.0.4", - "@libsql/client": "^0.17.2", - "@ticketpm/core": "^0.0.7", - "@ticketpm/discord-api": "^0.0.7", + "@libsql/client": "^0.17.3", + "@ticketpm/core": "^0.0.11", + "@ticketpm/discord-api": "^0.0.11", "discord-api-types": "^0.38.47", "dotenv": "^17.4.2", "drizzle-kit": "^0.31.10", @@ -38,9 +38,9 @@ "typesafe-i18n": "^5.27.1" }, "devDependencies": { - "@biomejs/biome": "^2.4.12", - "@types/node": "^24.9.1", + "@biomejs/biome": "^2.4.13", + "@types/node": "^25.6.0", "resolve-tspaths": "^0.8.23", - "typescript": "^6.0.2" + "typescript": "^6.0.3" } } From 279462f69ec3670e32d64404671ddda715b44ee2 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 1 May 2026 13:38:56 +0200 Subject: [PATCH 60/67] feat(ticketpm): hoist the TicketPmUploadClient creation in order to have a shared cache for avatar uploads and speed up the transcript creation process --- src/features/tickets/transcripts.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/features/tickets/transcripts.ts b/src/features/tickets/transcripts.ts index 863948a9..6032715a 100644 --- a/src/features/tickets/transcripts.ts +++ b/src/features/tickets/transcripts.ts @@ -26,6 +26,11 @@ const TRANSCRIPT_TIMEOUT_MS = 15 * 60 * 1000; type TranscriptStatusHandler = (content: string) => Promise | void; type TranscriptSourceMessage = Parameters[0]["messages"][number]; +const uploadClient = new TicketPmUploadClient({ + baseUrl: TRANSCRIPT_BASE_URL, + token: process.env.TICKETPM_PASSKEY +}); + export async function startTranscriptJob( app: BotApp, ticketChannelId: string, @@ -120,10 +125,6 @@ async function createTranscript(app: BotApp, channelId: string, onStatus?: Trans await reportStatus(onStatus, app.LL.tickets.transcript.uploading()); - const uploadClient = new TicketPmUploadClient({ - baseUrl: TRANSCRIPT_BASE_URL, - token: process.env.TICKETPM_PASSKEY - }); const result = await uploadClient.uploadDraftTranscript(draftTranscript, { uuidStyleIds: app.config.uuidType !== "emoji", avatarProgress: createProgressHandler(app, app.LL.tickets.transcript.uploading_avatars(), onStatus), From a35a3d7536992957dbc4fbb8a2e80f552cdbb8a8 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 3 May 2026 13:50:04 +0200 Subject: [PATCH 61/67] chore: add .vscode/ to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f8a6d39a..f40fa79a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ node_modules/ dist/ +.vscode/ + config/.env* config/config.ts* From 7d314ae04b47dd0279ab87c59043104b8b87911c Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 3 May 2026 13:50:44 +0200 Subject: [PATCH 62/67] Delete .vscode/settings.json --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 0bbb376e..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cSpell.words": ["bunx", "cleardm", "libsql", "replyable", "ticketbot", "ticketpm", "tsgo", "typesafe"] -} From cfab49f2509196eb43e1e2be626df9dfd8a9fd69 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 3 May 2026 13:51:26 +0200 Subject: [PATCH 63/67] chore: remove .data/.gitkeep and update .gitignore to exclude the entire .data directory --- .data/.gitkeep | 0 .gitignore | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .data/.gitkeep diff --git a/.data/.gitkeep b/.data/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/.gitignore b/.gitignore index f40fa79a..1de3f77a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ dist/ config/.env* config/config.ts* -.data/*.db \ No newline at end of file +.data/ \ No newline at end of file From d5095d0e99fba41ef397a2be08a4bb8ce564bf8d Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 3 May 2026 13:54:51 +0200 Subject: [PATCH 64/67] feat: add escapeDiscordMarkdown utility function and use it to sanitize closer's username in closeTicket --- src/features/tickets/close-workflow.ts | 4 ++-- src/features/tickets/utils.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/features/tickets/close-workflow.ts b/src/features/tickets/close-workflow.ts index 289ded1a..68ff715e 100644 --- a/src/features/tickets/close-workflow.ts +++ b/src/features/tickets/close-workflow.ts @@ -41,7 +41,7 @@ import { getInvitedUserIds, revokeTicketParticipantAccess } from "@/features/tic import { findTicketByChannel, getOpenTicketByChannel } from "@/features/tickets/records"; import { formatClaimStatus, formatTranscriptStatus, getDefaultNoReason } from "@/features/tickets/text"; import { startTranscriptJob } from "@/features/tickets/transcripts"; -import { getInteractionUser, getMemberRoleIds, getModalTextInputValues } from "@/features/tickets/utils"; +import { escapeDiscordMarkdown, getInteractionUser, getMemberRoleIds, getModalTextInputValues } from "@/features/tickets/utils"; const DEFAULT_CLOSE_DM_MESSAGE = "tickets/ticket-closed-dm"; const DEFAULT_CLOSE_CHANNEL_MESSAGE = "tickets/ticket-closed"; @@ -231,7 +231,7 @@ async function closeTicket( claimerMention: ticket.claimedBy ? `<@${ticket.claimedBy}>` : "", closerId: closer.id, closerMention: `<@${closer.id}>`, - closerName: closer.username, + closerName: escapeDiscordMarkdown(closer.username), reason: normalizedReason, transcriptStatus: formatTranscriptStatus(app, transcriptUrl), transcriptUrl: transcriptUrl ?? "", diff --git a/src/features/tickets/utils.ts b/src/features/tickets/utils.ts index a004427b..f72901d7 100644 --- a/src/features/tickets/utils.ts +++ b/src/features/tickets/utils.ts @@ -25,6 +25,10 @@ export function renderChannelName(template: string, tokens: Record])/g, "\\$1"); +} + export function sanitizeChannelName(value: string) { const cleaned = value .trim() From 11918f6f69f4ae2408a4fb30fec61a2fdd874894 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 3 May 2026 14:07:29 +0200 Subject: [PATCH 65/67] feat: create the folder where the db file lives if it does not exist --- drizzle.config.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/drizzle.config.ts b/drizzle.config.ts index f9ee5d0c..68dd2f92 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -13,17 +13,28 @@ project repository or to its website. This notice must not be removed, obscured, or replaced. */ +import { mkdirSync } from "node:fs"; +import { dirname, isAbsolute, resolve } from "node:path"; import { config } from "dotenv"; import { defineConfig } from "drizzle-kit"; config({ path: "./config/.env" }); +const databaseUrl = process.env.DB_FILE_NAME; + +if (databaseUrl?.startsWith("file:")) { + const databasePath = databaseUrl.slice("file:".length); + const databaseDirectory = dirname(isAbsolute(databasePath) ? databasePath : resolve(databasePath)); + + mkdirSync(databaseDirectory, { recursive: true }); +} + export default defineConfig({ out: "./drizzle", schema: "./src/db/schema.ts", dialect: "sqlite", dbCredentials: { - url: process.env.DB_FILE_NAME + url: databaseUrl } }); From 8d31c1e99950a17581d115401c700af145d6ad00 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 3 May 2026 14:07:44 +0200 Subject: [PATCH 66/67] chore: run format:fix --- scripts/verify-migrate-v3.mjs | 13 +++++-------- src/migrate-v3-config.ts | 10 +++++++--- src/migrate-v3-db.ts | 8 +++++--- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/scripts/verify-migrate-v3.mjs b/scripts/verify-migrate-v3.mjs index d5953879..70ff3553 100644 --- a/scripts/verify-migrate-v3.mjs +++ b/scripts/verify-migrate-v3.mjs @@ -32,13 +32,7 @@ async function verifyBasicMigration() { const targetUrl = await resetFixture("migrate-v3-basic-target.db"); await createV3Database(sourceUrl, validTypeKey, { includePanelMessage: Boolean(firstPanelKey) }); - await runMigration([ - "--source", - sourceUrl, - "--target", - targetUrl, - ...(firstPanelKey ? ["--panel-key", firstPanelKey] : []) - ]); + await runMigration(["--source", sourceUrl, "--target", targetUrl, ...(firstPanelKey ? ["--panel-key", firstPanelKey] : [])]); await withClient(targetUrl, async (target) => { const tickets = await target.execute("SELECT * FROM tickets ORDER BY id ASC"); @@ -49,7 +43,10 @@ async function verifyBasicMigration() { assert(tickets.rows[1].claimedBy === "222222222222222222", "basic migration should copy claimedBy."); assert(tickets.rows[1].closedReason === "Done", "basic migration should copy close reasons."); assert(tickets.rows[1].transcriptUrl === "https://ticket.pm/transcript/abc", "basic migration should copy transcript URLs."); - assert(tickets.rows[0].invitedUserIds === JSON.stringify(["333333333333333333"]), "basic migration should copy invited users."); + assert( + tickets.rows[0].invitedUserIds === JSON.stringify(["333333333333333333"]), + "basic migration should copy invited users." + ); if (firstPanelKey) { const panels = await target.execute("SELECT * FROM panel_messages"); diff --git a/src/migrate-v3-config.ts b/src/migrate-v3-config.ts index c0fadbed..94233e66 100644 --- a/src/migrate-v3-config.ts +++ b/src/migrate-v3-config.ts @@ -317,7 +317,9 @@ function migrateClaims(claimOption: V3Config["claimOption"]) { mode: "soft", showButtons: claimButton, allowUnclaim: true, - nameWhenClaimed: migrateClaimTemplate(readOptionalString(claimOption?.nameWhenClaimed) || "{ticketNumber}-claimed-{claimerUsername}"), + nameWhenClaimed: migrateClaimTemplate( + readOptionalString(claimOption?.nameWhenClaimed) || "{ticketNumber}-claimed-{claimerUsername}" + ), categoryWhenClaimed: categoryWhenClaimed || undefined, takeoverMode: "staff", takeoverRoleIds: [] @@ -506,7 +508,8 @@ function printSummary(summary: MigrationSummary) { } function printHelp() { - console.log(` + console.log( + ` Usage: bun run migrate:v3:config -- --source config.jsonc --output config/config.ts bun run migrate:v3:config -- --source config.jsonc --output .data/config.v4.ts @@ -517,7 +520,8 @@ Options: --panel-key v4 panel key created from openTicketChannelId. Defaults to support. --overwrite Replace the output file if it already exists. --help Show this help. -`.trim()); +`.trim() + ); } function licenseHeader() { diff --git a/src/migrate-v3-db.ts b/src/migrate-v3-db.ts index 0116f4c6..b5e82ead 100644 --- a/src/migrate-v3-db.ts +++ b/src/migrate-v3-db.ts @@ -399,7 +399,7 @@ async function preparePanelMigration(source: Client, requestedPanelKey: string | if (!openTicketMessageId) { return { - skippedReason: 'v3 config.openTicketMessageId was not found.' + skippedReason: "v3 config.openTicketMessageId was not found." }; } @@ -626,7 +626,8 @@ function printSummary(summary: MigrationSummary) { } function printHelp() { - console.log(` + console.log( + ` Usage: bun run migrate:v3 -- --source file:./tixbot.db bun run migrate:v3 -- --source file:./tixbot.db --target file:.data/sqlite.db @@ -638,7 +639,8 @@ Options: --type-map old=new Map a v3 ticket type codeName to a v4 ticketTypes key. Can be repeated. --overwrite Clear v4 tickets and panel_messages before importing. --help Show this help. -`.trim()); +`.trim() + ); } /* From aed78ff50bebeb45ec21173b40c89e690b0dd903 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Tue, 5 May 2026 15:41:39 +0200 Subject: [PATCH 67/67] fix(tickets-panel): keep panel opener inside components v2 container --- src/features/tickets/messages.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/features/tickets/messages.ts b/src/features/tickets/messages.ts index 536fc748..366f6842 100644 --- a/src/features/tickets/messages.ts +++ b/src/features/tickets/messages.ts @@ -16,8 +16,9 @@ This notice must not be removed, obscured, or replaced. import { access } from "node:fs/promises"; import { extname, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import type { APIButtonComponentWithCustomId, APIMessageTopLevelComponent } from "@discordjs/core"; -import { ComponentType, MessageFlags } from "@discordjs/core"; +import type { APIButtonComponentWithCustomId, APIMessageTopLevelComponent } from "discord-api-types/v10"; +import { ComponentType, MessageFlags } from "discord-api-types/v10"; +import type { BotApp } from "@/core/types"; import { MESSAGE_TEMPLATES_DIRECTORY } from "@/features/tickets/constants"; import type { DiscordMessageTemplate, @@ -27,7 +28,6 @@ import type { MessageTemplateSource } from "@/features/tickets/types"; import { renderTemplate } from "@/features/tickets/utils"; -import type { BotApp } from "@/core/types"; const TEMPLATE_SLOT_TYPE = "template-slot"; const TEMPLATE_SLOT_KIND_MANY = "many"; @@ -180,15 +180,13 @@ export function appendPanelOpener( }; } - if (usesComponentsV2(payload)) { - const containerInjection = appendComponentsToFirstContainer(currentComponents, components); + const containerInjection = appendComponentsToFirstContainer(currentComponents, components); - if (containerInjection.replaced) { - return { - ...payload, - components: containerInjection.value - }; - } + if (containerInjection.replaced) { + return { + ...payload, + components: containerInjection.value + }; } return appendMessageComponents(payload, components); @@ -370,9 +368,11 @@ function injectManyIntoSlots( return value; }; + const value = visit(components) as MessageTemplateComponent[]; + return { replaced, - value: visit(components) as MessageTemplateComponent[] + value }; } @@ -411,9 +411,11 @@ function appendComponentsToFirstContainer( return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry)])); }; + const value = visit(components) as MessageTemplateComponent[]; + return { replaced, - value: visit(components) as MessageTemplateComponent[] + value }; }