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/.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/.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
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
diff --git a/.gitignore b/.gitignore
index 7bb504db..1de3f77a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,9 @@
-node_modules
-config.jsonc
-token.json
-dist
-.env
-*.db
+node_modules/
+dist/
+.vscode/
-test.sql
-__pycache__/
-*.py[cod]
-local_settings.py
-.venv
+config/.env*
+config/config.ts*
+
+.data/
\ 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..eef6ee59
--- /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": "// biome-ignore-all lint: autogenerated file"
+}
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/LICENSE.md b/LICENSE.md
index 311e8a4f..22171517 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,157 +1,672 @@
-# 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
+ 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/README.md b/README.md
index 9a191f77..4ff500ef 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,16 @@
-# Ticket Bot
+# Ticket-Bot
-Ticket Bot is a open source project of an ticket discord bot using [discord.js](https://discord.js.org) v14
+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`.
-
-[](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
+The documentation is available [here](https://github.com/Sayrix/Ticket-Bot/wiki)
## 💬 Discord
-You can come on the discord: https://discord.gg/VasYV6MEJy
+Ask questions and get support on our [Discord server](https://discord.gg/VasYV6MEJy).
## ✨ Contributing
@@ -37,10 +31,20 @@ You can see all perks here: https://github.com/sponsors/Sayrix
## 🎥 Videos
-[Tutorial in french + english subtitle](https://youtu.be/24zAFj8w9gE?si=OvikXeNIJglz4FJV)
+- 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
-[](https://app.fossa.com/projects/git%2Bgithub.com%2FSayrix%2FTicket-Bot?ref=badge_large)
+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
diff --git a/biome.json b/biome.json
new file mode 100644
index 00000000..5bd60aea
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,54 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.4.12/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/", "!**/dist/", "!**/bun.lock"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "tab",
+ "indentWidth": 2,
+ "lineWidth": 130,
+ "includes": ["**", "!**/node_modules/", "!**/dist/", "!**/bun.lock"]
+ },
+ "html": {
+ "formatter": {
+ "enabled": true
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "trailingCommas": "none"
+ },
+ "globals": ["Bun"]
+ },
+ "css": {
+ "parser": {
+ "tailwindDirectives": true
+ },
+ "formatter": {
+ "enabled": false
+ }
+ },
+ "files": {
+ "includes": ["**", "!**/node_modules/", "!**/dist/", "!**/bun.lock"]
+ }
+}
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 00000000..8824e786
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,367 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "dependencies": {
+ "@discordjs/core": "2.4.0",
+ "@discordjs/rest": "^2.6.1",
+ "@discordjs/ws": "^2.0.4",
+ "@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",
+ "drizzle-orm": "^0.45.2",
+ "jsonc-parser": "^3.3.1",
+ "typesafe-i18n": "^5.27.1",
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.4.13",
+ "@types/node": "^25.6.0",
+ "resolve-tspaths": "^0.8.23",
+ "typescript": "^6.0.3",
+ },
+ },
+ },
+ "packages": {
+ "@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.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2KImO1jhNFBa2oWConyr0x6flxbQpGKv6902uGXpYM62Xyem8U80j441SyUJ8KyngsmKbQjeIv1q2CQfDkNnYg=="],
+
+ "@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.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ=="],
+
+ "@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.13", "", { "os": "linux", "cpu": "x64" }, "sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ=="],
+
+ "@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.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A=="],
+
+ "@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=="],
+
+ "@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=="],
+
+ "@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.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.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.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=="],
+
+ "@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=="],
+
+ "@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=="],
+
+ "@ticketpm/core": ["@ticketpm/core@0.0.11", "", {}, "sha512-VFOkwgzEEYtnMzAAR7z9hTb2ZaRfyUjb96i1JCd9EdrxuQaMZpU5MtktmeEg9wx2hs/9smS8zcw5W96xnqR9Xw=="],
+
+ "@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@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=="],
+
+ "@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=="],
+
+ "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=="],
+
+ "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=="],
+
+ "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=="],
+
+ "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=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "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=="],
+
+ "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=="],
+
+ "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=="],
+
+ "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=="],
+
+ "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.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.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=="],
+
+ "@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=="],
+
+ "@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=="],
+
+ "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=="],
+
+ "@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=="],
+
+ "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/config/config.example.ts b/config/config.example.ts
new file mode 100644
index 00000000..bb1c2c6f
--- /dev/null
+++ b/config/config.example.ts
@@ -0,0 +1,311 @@
+/*
+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.js";
+
+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",
+ // Supported locales: "en", "fr"
+ lang: "en",
+ // 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,
+ // 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,
+ // 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.
+ url: "https://twitch.tv/example",
+ // online, idle, dnd, invisible
+ status: "online"
+ },
+
+ 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"],
+ // 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:
+ // {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: {
+ 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",
+ // 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",
+ // Global fallback template path posted in the closed ticket channel.
+ // A ticket type can override this with ticketTypes..close.channelMessage.
+ 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.
+ // Available parameters here:
+ // {ticketNumber} {ticketTypeKey} {ticketTypeName} {userId} {username}
+ channelNameTemplate: "{ticketNumber}-general-{username}",
+ // 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:
+ // {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"],
+ // 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",
+ // 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",
+ 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",
+ 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: [
+ {
+ 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",
+ // 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.",
+ 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"
+ }
+ ]
+ }
+ }
+ }
+});
+
+/*
+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/config/example.env b/config/example.env
new file mode 100644
index 00000000..d68bbbcd
--- /dev/null
+++ b/config/example.env
@@ -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/docker-compose.yml b/docker-compose.yml
index c4c965f7..3262d345 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,28 +1,15 @@
-version: '3'
+name: ticket-bot
services:
bot:
- build: .
container_name: ticket-bot
- environment:
- - TOKEN
- networks:
- - net
- depends_on:
- - pgsql
- restart: on-failure
+ build:
+ context: .
+ dockerfile: Dockerfile
+ restart: unless-stopped
+ env_file:
+ - ./config/.env
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
+ - ./config:/app/config:ro
+ - ./messages:/app/messages:ro
+ - ./.data:/app/.data
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..68dd2f92
--- /dev/null
+++ b/drizzle.config.ts
@@ -0,0 +1,54 @@
+/*
+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 { 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: databaseUrl
+ }
+});
+
+/*
+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/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: {
+ /**
+ * An unexpected error occurred while handling this interaction.
+ */
+ unexpected_interaction_error: string;
+ /**
+ * No additional details were provided.
+ */
+ no_reason_provided: string;
+ claim_status: {
+ /**
+ * Claimed by <@{userId}>
+ * @param {string} userId
+ */
+ claimed_by: RequiredParams<"userId">;
+ /**
+ * Unclaimed
+ */
+ unclaimed: string;
+ };
+ transcript_status: {
+ /**
+ * [Open Transcript]({url})
+ * @param {string} url
+ */
+ ready: RequiredParams<"url">;
+ /**
+ * Unavailable or still processing.
+ */
+ unavailable: string;
+ };
+ };
+ commands: {
+ add: {
+ /**
+ * Add someone to the current ticket
+ */
+ description: string;
+ options: {
+ user: {
+ /**
+ * The user to add
+ */
+ description: string;
+ };
+ };
+ /**
+ * Choose a user to add to this ticket.
+ */
+ choose_user: string;
+ /**
+ * That user already has access to this ticket.
+ */
+ already_has_access: string;
+ /**
+ * That user is already invited to this ticket.
+ */
+ already_invited: string;
+ /**
+ * You cannot invite more than {limit} users to one ticket.
+ * @param {number} limit
+ */
+ invite_limit_reached: RequiredParams<"limit">;
+ /**
+ * Added <@{userId}> to this ticket.
+ * @param {string} userId
+ */
+ success: RequiredParams<"userId">;
+ };
+ claim: {
+ /**
+ * Claim the current ticket
+ */
+ description: string;
+ /**
+ * Ticket claiming is disabled.
+ */
+ disabled: string;
+ /**
+ * You already claimed this ticket.
+ */
+ already_claimed: string;
+ /**
+ * This ticket is already claimed and cannot be taken over.
+ */
+ cannot_take_over: string;
+ /**
+ * Only staff can claim this ticket.
+ */
+ only_staff: string;
+ /**
+ * You claimed this ticket.
+ */
+ success: string;
+ /**
+ * Ticket reassigned to <@{userId}>.
+ * @param {string} userId
+ */
+ reassigned: RequiredParams<"userId">;
+ };
+ close: {
+ /**
+ * Close the current ticket
+ */
+ description: string;
+ };
+ cleardm: {
+ /**
+ * Clear the bot's ticket history from your DMs
+ */
+ description: string;
+ /**
+ * Clearing your ticket DM history...
+ */
+ starting: string;
+ /**
+ * I could not access your DM channel.
+ */
+ dm_unavailable: string;
+ /**
+ * Cleared {count} ticket DM messages.
+ * @param {number} count
+ */
+ cleared: RequiredParams<"count">;
+ /**
+ * No ticket DM messages were found.
+ */
+ none_found: string;
+ };
+ mass_add: {
+ /**
+ * Add multiple users to the current ticket
+ */
+ description: string;
+ options: {
+ users: {
+ /**
+ * Comma-separated user IDs or mentions
+ */
+ description: string;
+ };
+ };
+ /**
+ * Provide at least one user ID or mention.
+ */
+ provide_users: string;
+ summary: {
+ /**
+ * Added {mentions}.
+ * @param {string} mentions
+ */
+ added: RequiredParams<"mentions">;
+ /**
+ * No users were added.
+ */
+ none_added: string;
+ /**
+ * Skipped {count} user(s) that already had access.
+ * @param {number} count
+ */
+ skipped_existing: RequiredParams<"count">;
+ /**
+ * Skipped {count} invalid user ID(s).
+ * @param {number} count
+ */
+ skipped_invalid: RequiredParams<"count">;
+ /**
+ * Stopped when the {limit}-user ticket limit was reached.
+ * @param {number} limit
+ */
+ limit_reached: RequiredParams<"limit">;
+ };
+ };
+ remove: {
+ /**
+ * Remove invited users from the current ticket
+ */
+ description: string;
+ options: {
+ user: {
+ /**
+ * The invited user to remove immediately
+ */
+ description: string;
+ };
+ };
+ /**
+ * There are no invited users to remove from this ticket.
+ */
+ no_invited_users: string;
+ /**
+ * Select the invited users you want to remove from this ticket.
+ */
+ select_users: string;
+ /**
+ * Choose users to remove
+ */
+ select_placeholder: string;
+ /**
+ * Those users are not invited to this ticket.
+ */
+ not_invited: string;
+ /**
+ * Removed {mentions} from this ticket.
+ * @param {string} mentions
+ */
+ success: RequiredParams<"mentions">;
+ };
+ rename: {
+ /**
+ * Rename the current ticket
+ */
+ description: string;
+ options: {
+ name: {
+ /**
+ * The new ticket channel name
+ */
+ description: string;
+ };
+ };
+ /**
+ * Only staff can rename this ticket.
+ */
+ only_staff: string;
+ /**
+ * Provide a new ticket name.
+ */
+ provide_name: string;
+ /**
+ * Ticket renamed to <#{channelId}>.
+ * @param {string} channelId
+ */
+ success: RequiredParams<"channelId">;
+ };
+ unclaim: {
+ /**
+ * Unclaim the current ticket
+ */
+ description: string;
+ /**
+ * Unclaiming is disabled for this server.
+ */
+ disabled: string;
+ /**
+ * This ticket is not claimed.
+ */
+ not_claimed: string;
+ /**
+ * Only the current claimer can unclaim this ticket.
+ */
+ only_current_claimer: string;
+ /**
+ * You unclaimed this ticket.
+ */
+ success: string;
+ };
+ };
+ tickets: {
+ records: {
+ /**
+ * This interaction was not used in a ticket channel.
+ */
+ not_ticket_channel: string;
+ /**
+ * This channel is not an open ticket.
+ */
+ not_open_ticket: string;
+ /**
+ * This ticket is already closed.
+ */
+ already_closed: string;
+ };
+ panel: {
+ /**
+ * You do not have access to any ticket types on this panel.
+ */
+ no_visible_types: string;
+ /**
+ * Please select a ticket type.
+ */
+ select_type: string;
+ /**
+ * That ticket type is not available from this panel.
+ */
+ unavailable_type: string;
+ /**
+ * Select a ticket type
+ */
+ select_placeholder: string;
+ };
+ open: {
+ /**
+ * You are not allowed to create that ticket type.
+ */
+ not_allowed_type: string;
+ /**
+ * That ticket type is not available from this panel.
+ */
+ unavailable_type: string;
+ /**
+ * You already have the maximum number of open tickets ({limit}).
+ * @param {number} limit
+ */
+ max_open_reached: RequiredParams<"limit">;
+ /**
+ * Your ticket has been created: <#{channelId}>
+ * @param {string} channelId
+ */
+ created: RequiredParams<"channelId">;
+ /**
+ * {label}: {answer}
+ * @param {string} answer
+ * @param {string} label
+ */
+ question_answer: RequiredParams<"answer" | "label">;
+ };
+ claim: {
+ /**
+ * Only staff can claim this ticket.
+ */
+ only_staff: string;
+ };
+ actions: {
+ /**
+ * Close Ticket
+ */
+ close_ticket: string;
+ /**
+ * Claim Ticket
+ */
+ claim_ticket: string;
+ /**
+ * Unclaim Ticket
+ */
+ unclaim_ticket: string;
+ /**
+ * Delete Ticket
+ */
+ delete_ticket: string;
+ };
+ close: {
+ /**
+ * Deleting ticket channel...
+ */
+ delete_channel_start: string;
+ modal: {
+ /**
+ * Close Ticket
+ */
+ title: string;
+ /**
+ * Reason
+ */
+ reason_label: string;
+ /**
+ * Why is this ticket being closed?
+ */
+ reason_placeholder: string;
+ };
+ status: {
+ /**
+ * Preparing transcript...
+ */
+ preparing_transcript: string;
+ /**
+ * Closing ticket...
+ */
+ closing_ticket: string;
+ /**
+ * Updating ticket access...
+ */
+ updating_access: string;
+ /**
+ * Transcript is still processing. Finishing ticket close...
+ */
+ transcript_still_processing: string;
+ /**
+ * Sending close confirmation...
+ */
+ sending_close_confirmation: string;
+ /**
+ * Sending close updates...
+ */
+ sending_close_updates: string;
+ /**
+ * Posting close summary...
+ */
+ posting_close_summary: string;
+ /**
+ * Ticket closed.
+ */
+ closed: string;
+ };
+ /**
+ * Ticket closed. The transcript is ready and the channel will now be deleted.
+ */
+ deleted_with_transcript: string;
+ /**
+ * Ticket closed. The channel will now be deleted.
+ */
+ deleted_without_transcript: string;
+ /**
+ * Only staff can close this ticket.
+ */
+ only_staff: string;
+ /**
+ * This ticket must be claimed before it can be closed.
+ */
+ must_be_claimed: string;
+ /**
+ * Only the current claimer can close this ticket.
+ */
+ only_current_claimer: string;
+ /**
+ * This channel is not a ticket.
+ */
+ not_ticket: string;
+ /**
+ * Only closed tickets can be deleted from this button.
+ */
+ only_closed_delete: string;
+ /**
+ * Only staff can delete this ticket.
+ */
+ only_staff_delete: string;
+ };
+ transcript: {
+ /**
+ * Collecting ticket messages...
+ */
+ collecting_messages: string;
+ /**
+ * Creating transcript...
+ */
+ creating: string;
+ /**
+ * Uploading transcript...
+ */
+ uploading: string;
+ /**
+ * Uploading avatars...
+ */
+ uploading_avatars: string;
+ /**
+ * Uploading attachments...
+ */
+ uploading_attachments: string;
+ /**
+ * {label} ({completed}/{total})
+ * @param {number} completed
+ * @param {string} label
+ * @param {number} total
+ */
+ progress: RequiredParams<"completed" | "label" | "total">;
+ };
+ templates: {
+ open_panel: {
+ /**
+ * ## Open a Ticket
+ */
+ title: string;
+ /**
+ * Choose the category that matches your request and the bot will create a private ticket for you.
+ */
+ description: string;
+ };
+ ticket_opened: {
+ /**
+ * ## {ticketTypeName} Ticket
+ * @param {string} ticketTypeName
+ */
+ title: RequiredParams<"ticketTypeName">;
+ /**
+ * Thanks for opening a ticket.
+ */
+ intro: string;
+ /**
+ * **Details**
+ {reason}
+ * @param {string} reason
+ */
+ details_label: RequiredParams<"reason">;
+ /**
+ * **Claim Status**: {claimStatus}
+ * @param {string} claimStatus
+ */
+ claim_status: RequiredParams<"claimStatus">;
+ };
+ ticket_opened_general: {
+ /**
+ * ## General Support Ticket
+ */
+ title: string;
+ /**
+ * A support team member will review this request soon.
+ */
+ intro: string;
+ /**
+ * **Summary**
+ {reason}
+ * @param {string} reason
+ */
+ details_label: RequiredParams<"reason">;
+ /**
+ * **Claim Status**: {claimStatus}
+ * @param {string} claimStatus
+ */
+ claim_status: RequiredParams<"claimStatus">;
+ };
+ ticket_opened_billing: {
+ /**
+ * ## Billing Ticket
+ */
+ title: string;
+ /**
+ * Include invoice numbers, payment method, and any failed transaction details.
+ */
+ intro: string;
+ /**
+ * **Submitted Details**
+ {reason}
+ * @param {string} reason
+ */
+ details_label: RequiredParams<"reason">;
+ /**
+ * **Claim Status**: {claimStatus}
+ * @param {string} claimStatus
+ */
+ claim_status: RequiredParams<"claimStatus">;
+ };
+ ticket_opened_report: {
+ /**
+ * ## Report Ticket
+ */
+ title: string;
+ /**
+ * Moderation staff will review the report and any evidence attached.
+ */
+ intro: string;
+ /**
+ * **Report Details**
+ {reason}
+ * @param {string} reason
+ */
+ details_label: RequiredParams<"reason">;
+ /**
+ * **Claim Status**: {claimStatus}
+ * @param {string} claimStatus
+ */
+ claim_status: RequiredParams<"claimStatus">;
+ };
+ ticket_closed: {
+ /**
+ * ## Ticket Closed
+ */
+ title: string;
+ /**
+ * <@{userId}>'s ticket has been closed.
+ * @param {string} userId
+ */
+ subtitle: RequiredParams<"userId">;
+ /**
+ * **Reason**: {reason}
+ **Claim**: {claimStatus}
+ **Transcript**: {transcriptStatus}
+ * @param {string} claimStatus
+ * @param {string} reason
+ * @param {string} transcriptStatus
+ */
+ details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">;
+ /**
+ * -# _Closed by {closerName}_
+ * @param {string} closerName
+ */
+ closed_by: RequiredParams<"closerName">;
+ };
+ ticket_closed_general: {
+ /**
+ * ## General Support Closed
+ */
+ title: string;
+ /**
+ * <@{userId}>'s general support ticket is now closed.
+ * @param {string} userId
+ */
+ subtitle: RequiredParams<"userId">;
+ /**
+ * **Reason**: {reason}
+ **Claim**: {claimStatus}
+ **Transcript**: {transcriptStatus}
+ * @param {string} claimStatus
+ * @param {string} reason
+ * @param {string} transcriptStatus
+ */
+ details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">;
+ /**
+ * -# _Closed by {closerName}_
+ * @param {string} closerName
+ */
+ closed_by: RequiredParams<"closerName">;
+ };
+ ticket_closed_billing: {
+ /**
+ * ## Billing Ticket Closed
+ */
+ title: string;
+ /**
+ * <@{userId}>'s billing ticket has been closed.
+ * @param {string} userId
+ */
+ subtitle: RequiredParams<"userId">;
+ /**
+ * **Close Reason**: {reason}
+ **Claim**: {claimStatus}
+ **Transcript**: {transcriptStatus}
+ * @param {string} claimStatus
+ * @param {string} reason
+ * @param {string} transcriptStatus
+ */
+ details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">;
+ /**
+ * -# _Closed by {closerName}_
+ * @param {string} closerName
+ */
+ closed_by: RequiredParams<"closerName">;
+ };
+ ticket_closed_report: {
+ /**
+ * ## Report Case Closed
+ */
+ title: string;
+ /**
+ * The report opened by <@{userId}> has been closed.
+ * @param {string} userId
+ */
+ subtitle: RequiredParams<"userId">;
+ /**
+ * **Resolution Note**: {reason}
+ **Claim**: {claimStatus}
+ **Transcript**: {transcriptStatus}
+ * @param {string} claimStatus
+ * @param {string} reason
+ * @param {string} transcriptStatus
+ */
+ details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">;
+ /**
+ * -# _Closed by {closerName}_
+ * @param {string} closerName
+ */
+ closed_by: RequiredParams<"closerName">;
+ };
+ ticket_closed_dm: {
+ /**
+ * ## Your ticket has been closed
+ */
+ title: string;
+ /**
+ * **Reason**: {reason}
+ **Claim**: {claimStatus}
+ **Transcript**: {transcriptStatus}
+ * @param {string} claimStatus
+ * @param {string} reason
+ * @param {string} transcriptStatus
+ */
+ details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">;
+ /**
+ * -# _Closed by {closerName}_
+ * @param {string} closerName
+ */
+ closed_by: RequiredParams<"closerName">;
+ };
+ ticket_closed_dm_general: {
+ /**
+ * ## Your general support ticket has been closed
+ */
+ title: string;
+ /**
+ * **Reason**: {reason}
+ **Claim**: {claimStatus}
+ **Transcript**: {transcriptStatus}
+ * @param {string} claimStatus
+ * @param {string} reason
+ * @param {string} transcriptStatus
+ */
+ details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">;
+ /**
+ * -# _Closed by {closerName}_
+ * @param {string} closerName
+ */
+ closed_by: RequiredParams<"closerName">;
+ };
+ ticket_closed_dm_billing: {
+ /**
+ * ## Your billing ticket has been closed
+ */
+ title: string;
+ /**
+ * If you still need help, open a new billing ticket and include your order details again.
+ */
+ intro: string;
+ /**
+ * **Reason**: {reason}
+ **Claim**: {claimStatus}
+ **Transcript**: {transcriptStatus}
+ * @param {string} claimStatus
+ * @param {string} reason
+ * @param {string} transcriptStatus
+ */
+ details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">;
+ /**
+ * -# _Closed by {closerName}_
+ * @param {string} closerName
+ */
+ closed_by: RequiredParams<"closerName">;
+ };
+ ticket_closed_dm_report: {
+ /**
+ * ## Your report ticket has been closed
+ */
+ title: string;
+ /**
+ * Staff reviewed the report and any attached evidence.
+ */
+ intro: string;
+ /**
+ * **Resolution Note**: {reason}
+ **Claim**: {claimStatus}
+ **Transcript**: {transcriptStatus}
+ * @param {string} claimStatus
+ * @param {string} reason
+ * @param {string} transcriptStatus
+ */
+ details: RequiredParams<"claimStatus" | "reason" | "transcriptStatus">;
+ /**
+ * -# _Closed by {closerName}_
+ * @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: {
+ /**
+ * ## Ticket Created
+ */
+ title: string;
+ /**
+ * {actorMention} opened {ticketChannelMention}.
+ * @param {string} actorMention
+ * @param {string} ticketChannelMention
+ */
+ action: RequiredParams<"actorMention" | "ticketChannelMention">;
+ /**
+ * **Ticket**: #{ticketId} - {ticketTypeName}
+ **Opened By**: {createdByMention}
+ **Created**: {createdAt}
+ **Reason**: {reason}
+ * @param {string} createdAt
+ * @param {string} createdByMention
+ * @param {string} reason
+ * @param {string} ticketId
+ * @param {string} ticketTypeName
+ */
+ details: RequiredParams<"createdAt" | "createdByMention" | "reason" | "ticketId" | "ticketTypeName">;
+ };
+ ticket_claimed: {
+ /**
+ * ## Ticket Claimed
+ */
+ title: string;
+ /**
+ * {actorMention} claimed {ticketChannelMention}.
+ * @param {string} actorMention
+ * @param {string} ticketChannelMention
+ */
+ action: RequiredParams<"actorMention" | "ticketChannelMention">;
+ /**
+ * **Ticket**: #{ticketId} - {ticketTypeName}
+ **Opened By**: {createdByMention}
+ **Open Age**: {ticketAge}
+ * @param {string} createdByMention
+ * @param {string} ticketAge
+ * @param {string} ticketId
+ * @param {string} ticketTypeName
+ */
+ details: RequiredParams<"createdByMention" | "ticketAge" | "ticketId" | "ticketTypeName">;
+ };
+ ticket_unclaimed: {
+ /**
+ * ## Ticket Unclaimed
+ */
+ title: string;
+ /**
+ * {actorMention} unclaimed {ticketChannelMention}.
+ * @param {string} actorMention
+ * @param {string} ticketChannelMention
+ */
+ action: RequiredParams<"actorMention" | "ticketChannelMention">;
+ /**
+ * **Ticket**: #{ticketId} - {ticketTypeName}
+ **Opened By**: {createdByMention}
+ **Open Age**: {ticketAge}
+ * @param {string} createdByMention
+ * @param {string} ticketAge
+ * @param {string} ticketId
+ * @param {string} ticketTypeName
+ */
+ details: RequiredParams<"createdByMention" | "ticketAge" | "ticketId" | "ticketTypeName">;
+ };
+ ticket_closed: {
+ /**
+ * ## Ticket Closed
+ */
+ title: string;
+ /**
+ * {actorMention} closed {ticketChannelMention}.
+ * @param {string} actorMention
+ * @param {string} ticketChannelMention
+ */
+ action: RequiredParams<"actorMention" | "ticketChannelMention">;
+ /**
+ * **Ticket**: #{ticketId} - {ticketTypeName}
+ **Opened By**: {createdByMention}
+ **Claim Status**: {claimStatus}
+ **Open Age**: {ticketAge}
+ **Reason**: {reason}
+ **Transcript**: {transcriptStatus}
+ * @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: {
+ /**
+ * ## Ticket Deleted
+ */
+ title: string;
+ /**
+ * {actorMention} deleted {ticketChannelMention}.
+ * @param {string} actorMention
+ * @param {string} ticketChannelMention
+ */
+ action: RequiredParams<"actorMention" | "ticketChannelMention">;
+ /**
+ * **Ticket**: #{ticketId} - {ticketTypeName}
+ **Opened By**: {createdByMention}
+ **Claim Status**: {claimStatus}
+ **Open Age**: {ticketAge}
+ **Close Reason**: {reason}
+ **Transcript**: {transcriptStatus}
+ * @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: {
+ /**
+ * ## Ticket Renamed
+ */
+ title: string;
+ /**
+ * {actorMention} renamed {ticketChannelMention}.
+ * @param {string} actorMention
+ * @param {string} ticketChannelMention
+ */
+ action: RequiredParams<"actorMention" | "ticketChannelMention">;
+ /**
+ * **Ticket**: #{ticketId} - {ticketTypeName}
+ **Opened By**: {createdByMention}
+ **From**: `{oldChannelName}`
+ **To**: `{newChannelName}`
+ * @param {string} createdByMention
+ * @param {string} newChannelName
+ * @param {string} oldChannelName
+ * @param {string} ticketId
+ * @param {string} ticketTypeName
+ */
+ details: RequiredParams<"createdByMention" | "newChannelName" | "oldChannelName" | "ticketId" | "ticketTypeName">;
+ };
+ user_added: {
+ /**
+ * ## User Added
+ */
+ title: string;
+ /**
+ * {actorMention} added {targetMention} to {ticketChannelMention}.
+ * @param {string} actorMention
+ * @param {string} targetMention
+ * @param {string} ticketChannelMention
+ */
+ action: RequiredParams<"actorMention" | "targetMention" | "ticketChannelMention">;
+ /**
+ * **Ticket**: #{ticketId} - {ticketTypeName}
+ **Opened By**: {createdByMention}
+ * @param {string} createdByMention
+ * @param {string} ticketId
+ * @param {string} ticketTypeName
+ */
+ details: RequiredParams<"createdByMention" | "ticketId" | "ticketTypeName">;
+ };
+ user_removed: {
+ /**
+ * ## User Removed
+ */
+ title: string;
+ /**
+ * {actorMention} removed {targetMention} from {ticketChannelMention}.
+ * @param {string} actorMention
+ * @param {string} targetMention
+ * @param {string} ticketChannelMention
+ */
+ action: RequiredParams<"actorMention" | "targetMention" | "ticketChannelMention">;
+ /**
+ * **Ticket**: #{ticketId} - {ticketTypeName}
+ **Opened By**: {createdByMention}
+ * @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/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/messages/logs/ticket-claimed.ts b/messages/logs/ticket-claimed.ts
new file mode 100644
index 00000000..a2ff3747
--- /dev/null
+++ b/messages/logs/ticket-claimed.ts
@@ -0,0 +1,62 @@
+/*
+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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClaimedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16426522,
+ components: [
+ { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_claimed.title() },
+ {
+ type: ComponentType.TextDisplay,
+ 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;
+
+/*
+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..4ef607ae
--- /dev/null
+++ b/messages/logs/ticket-closed.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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16007990,
+ components: [
+ { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_closed.title() },
+ {
+ type: ComponentType.TextDisplay,
+ 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;
+
+/*
+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..8706e624
--- /dev/null
+++ b/messages/logs/ticket-created.ts
@@ -0,0 +1,63 @@
+/*
+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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketCreatedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 3901635,
+ components: [
+ { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_created.title() },
+ {
+ type: ComponentType.TextDisplay,
+ 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;
+
+/*
+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..d86795ca
--- /dev/null
+++ b/messages/logs/ticket-deleted.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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketDeletedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 13632027,
+ components: [
+ { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_deleted.title() },
+ {
+ type: ComponentType.TextDisplay,
+ 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;
+
+/*
+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..51c3fa48
--- /dev/null
+++ b/messages/logs/ticket-renamed.ts
@@ -0,0 +1,63 @@
+/*
+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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketRenamedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 3447003,
+ components: [
+ { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_renamed.title() },
+ {
+ type: ComponentType.TextDisplay,
+ 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;
+
+/*
+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..9e126eac
--- /dev/null
+++ b/messages/logs/ticket-unclaimed.ts
@@ -0,0 +1,62 @@
+/*
+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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketUnclaimedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 9807270,
+ components: [
+ { type: ComponentType.TextDisplay, content: LL.logs.templates.ticket_unclaimed.title() },
+ {
+ type: ComponentType.TextDisplay,
+ 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;
+
+/*
+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..56fb2e0d
--- /dev/null
+++ b/messages/logs/user-added.ts
@@ -0,0 +1,62 @@
+/*
+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, MessageTemplateContext } from "@/features/tickets/types";
+
+const userAddedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 3901635,
+ components: [
+ { type: ComponentType.TextDisplay, content: LL.logs.templates.user_added.title() },
+ {
+ type: ComponentType.TextDisplay,
+ 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;
+
+/*
+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..51ef287d
--- /dev/null
+++ b/messages/logs/user-removed.ts
@@ -0,0 +1,62 @@
+/*
+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, MessageTemplateContext } from "@/features/tickets/types";
+
+const userRemovedLogMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16007990,
+ components: [
+ { type: ComponentType.TextDisplay, content: LL.logs.templates.user_removed.title() },
+ {
+ type: ComponentType.TextDisplay,
+ 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;
+
+/*
+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
new file mode 100644
index 00000000..cf4f61d7
--- /dev/null
+++ b/messages/tickets/open-panel.ts
@@ -0,0 +1,64 @@
+/*
+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, MessageTemplateContext } from "@/features/tickets/types";
+
+const openPanelMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ useComponentsV2: true,
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.open_panel.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.open_panel.description()
+ },
+ {
+ type: ComponentType.Separator
+ },
+ {
+ type: ComponentType.Separator,
+ spacing: 1,
+ divider: false
+ },
+ createPanelOpenerSlot()
+ ]
+ }
+ ]
+});
+
+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-billing.ts b/messages/tickets/ticket-closed-billing.ts
new file mode 100644
index 00000000..b258c6be
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedBillingMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_billing.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_billing.subtitle({ userId: "{userId}" })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_billing.details({
+ reason: "{reason}",
+ claimStatus: "{claimStatus}",
+ transcriptStatus: "{transcriptStatus}"
+ })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_billing.closed_by({ closerName: "{closerName}" })
+ },
+ {
+ type: ComponentType.ActionRow,
+ components: [
+ {
+ type: ComponentType.Button,
+ custom_id: "{deleteButtonCustomId}",
+ label: LL.tickets.actions.delete_ticket(),
+ style: 4
+ }
+ ]
+ }
+ ]
+ }
+ ]
+});
+
+export default ticketClosedBillingMessage;
+
+/*
+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..d6baad42
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedDmBillingMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_billing.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_billing.intro()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_billing.details({
+ reason: "{reason}",
+ claimStatus: "{claimStatus}",
+ transcriptStatus: "{transcriptStatus}"
+ })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_billing.closed_by({ closerName: "{closerName}" })
+ }
+ ]
+ }
+ ]
+});
+
+export default ticketClosedDmBillingMessage;
+
+/*
+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..b4255703
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedDmGeneralMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_general.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_general.details({
+ reason: "{reason}",
+ claimStatus: "{claimStatus}",
+ transcriptStatus: "{transcriptStatus}"
+ })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_general.closed_by({ closerName: "{closerName}" })
+ }
+ ]
+ }
+ ]
+});
+
+export default ticketClosedDmGeneralMessage;
+
+/*
+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..0c878d95
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedDmReportMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_report.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_report.intro()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_report.details({
+ reason: "{reason}",
+ claimStatus: "{claimStatus}",
+ transcriptStatus: "{transcriptStatus}"
+ })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm_report.closed_by({ closerName: "{closerName}" })
+ }
+ ]
+ }
+ ]
+});
+
+export default ticketClosedDmReportMessage;
+
+/*
+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
new file mode 100644
index 00000000..848a4c45
--- /dev/null
+++ b/messages/tickets/ticket-closed-dm.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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedDmMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm.details({
+ reason: "{reason}",
+ claimStatus: "{claimStatus}",
+ transcriptStatus: "{transcriptStatus}"
+ })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_dm.closed_by({ closerName: "{closerName}" })
+ }
+ ]
+ }
+ ]
+});
+
+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-general.ts b/messages/tickets/ticket-closed-general.ts
new file mode 100644
index 00000000..a7a60123
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedGeneralMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_general.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_general.subtitle({ userId: "{userId}" })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_general.details({
+ reason: "{reason}",
+ claimStatus: "{claimStatus}",
+ transcriptStatus: "{transcriptStatus}"
+ })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_general.closed_by({ closerName: "{closerName}" })
+ },
+ {
+ type: ComponentType.ActionRow,
+ components: [
+ {
+ type: ComponentType.Button,
+ custom_id: "{deleteButtonCustomId}",
+ label: LL.tickets.actions.delete_ticket(),
+ style: 4
+ }
+ ]
+ }
+ ]
+ }
+ ]
+});
+
+export default ticketClosedGeneralMessage;
+
+/*
+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..4e821a6d
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedReportMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_report.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_report.subtitle({ userId: "{userId}" })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_report.details({
+ reason: "{reason}",
+ claimStatus: "{claimStatus}",
+ transcriptStatus: "{transcriptStatus}"
+ })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed_report.closed_by({ closerName: "{closerName}" })
+ },
+ {
+ type: ComponentType.ActionRow,
+ components: [
+ {
+ type: ComponentType.Button,
+ custom_id: "{deleteButtonCustomId}",
+ label: LL.tickets.actions.delete_ticket(),
+ style: 4
+ }
+ ]
+ }
+ ]
+ }
+ ]
+});
+
+export default ticketClosedReportMessage;
+
+/*
+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
new file mode 100644
index 00000000..5dda4ba7
--- /dev/null
+++ b/messages/tickets/ticket-closed.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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketClosedMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed.subtitle({ userId: "{userId}" })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed.details({
+ reason: "{reason}",
+ claimStatus: "{claimStatus}",
+ transcriptStatus: "{transcriptStatus}"
+ })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_closed.closed_by({ closerName: "{closerName}" })
+ },
+ {
+ type: ComponentType.ActionRow,
+ components: [
+ {
+ type: ComponentType.Button,
+ custom_id: "{deleteButtonCustomId}",
+ label: LL.tickets.actions.delete_ticket(),
+ style: 4
+ }
+ ]
+ }
+ ]
+ }
+ ]
+});
+
+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-billing.ts b/messages/tickets/ticket-opened-billing.ts
new file mode 100644
index 00000000..563f0447
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketOpenedBillingMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: "{createdByMention}{staffMentions}"
+ },
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_billing.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_billing.intro()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_billing.details_label({ reason: "{reason}" })
+ },
+ createRuntimeTextSlot(),
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_billing.claim_status({ claimStatus: "{claimStatus}" })
+ },
+ createMessageSlot("actions")
+ ]
+ }
+ ]
+});
+
+export default ticketOpenedBillingMessage;
+
+/*
+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..7f873781
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketOpenedGeneralMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: "{createdByMention}{staffMentions}"
+ },
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_general.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_general.intro()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_general.details_label({ reason: "{reason}" })
+ },
+ createRuntimeTextSlot(),
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_general.claim_status({ claimStatus: "{claimStatus}" })
+ },
+ createMessageSlot("actions")
+ ]
+ }
+ ]
+});
+
+export default ticketOpenedGeneralMessage;
+
+/*
+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..62932386
--- /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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketOpenedReportMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: "{createdByMention}{staffMentions}"
+ },
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_report.title()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_report.intro()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_report.details_label({ reason: "{reason}" })
+ },
+ createRuntimeTextSlot(),
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened_report.claim_status({ claimStatus: "{claimStatus}" })
+ },
+ createMessageSlot("actions")
+ ]
+ }
+ ]
+});
+
+export default ticketOpenedReportMessage;
+
+/*
+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
new file mode 100644
index 00000000..b9fb0d89
--- /dev/null
+++ b/messages/tickets/ticket-opened.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, MessageTemplateContext } from "@/features/tickets/types";
+
+const ticketOpenedMessage = ({ LL }: MessageTemplateContext): LoadedMessageTemplate => ({
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: "{createdByMention}{staffMentions}"
+ },
+ {
+ type: ComponentType.Container,
+ accent_color: 16106539,
+ components: [
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened.title({ ticketTypeName: "{ticketTypeName}" })
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened.intro()
+ },
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened.details_label({ reason: "{reason}" })
+ },
+ createRuntimeTextSlot(),
+ {
+ type: ComponentType.TextDisplay,
+ content: LL.tickets.templates.ticket_opened.claim_status({ claimStatus: "{claimStatus}" })
+ },
+ createMessageSlot("actions")
+ ]
+ }
+ ]
+});
+
+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/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..f0bb022a 100644
--- a/package.json
+++ b/package.json
@@ -1,63 +1,46 @@
{
- "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",
+ "license": "AGPL-3.0-only",
+ "description": "Open-source Discord ticket bot.",
+ "type": "module",
+ "main": "./dist/src/index.js",
+ "engines": {
+ "node": ">=20.0.0",
+ "bun": ">=1.0.0"
+ },
+ "scripts": {
+ "typecheck": "tsc --noEmit -p tsconfig.json",
+ "i18n": "typesafe-i18n --no-watch",
+ "lint": "biome lint .",
+ "format": "biome format .",
+ "format:fix": "biome format --write",
+ "drizzle:push": "drizzle-kit push",
+ "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",
+ "@discordjs/rest": "^2.6.1",
+ "@discordjs/ws": "^2.0.4",
+ "@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",
+ "drizzle-orm": "^0.45.2",
+ "jsonc-parser": "^3.3.1",
+ "typesafe-i18n": "^5.27.1"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.4.13",
+ "@types/node": "^25.6.0",
+ "resolve-tspaths": "^0.8.23",
+ "typescript": "^6.0.3"
+ }
}
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/scripts/verify-migrate-v3.mjs b/scripts/verify-migrate-v3.mjs
new file mode 100644
index 00000000..70ff3553
--- /dev/null
+++ b/scripts/verify-migrate-v3.mjs
@@ -0,0 +1,366 @@
+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/app.ts b/src/app.ts
new file mode 100644
index 00000000..7b6f5b41
--- /dev/null
+++ b/src/app.ts
@@ -0,0 +1,99 @@
+/*
+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";
+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";
+import type { BotApp } from "@/core/types";
+import botConfig from "../config/config.js";
+
+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 [commands, events, features] = await Promise.all([
+ discoverCommands(logger),
+ discoverEvents(logger),
+ discoverFeatures(logger)
+ ]);
+ 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;
+ }
+ };
+
+ 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);
+ }
+ }
+ };
+}
+
+/*
+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/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..dccf63ec
--- /dev/null
+++ b/src/config/index.ts
@@ -0,0 +1,181 @@
+/*
+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 { 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: Locales;
+ /** 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;
+ events?: {
+ ticketCreate?: boolean;
+ ticketClaim?: boolean;
+ ticketUnclaim?: boolean;
+ ticketClose?: boolean;
+ ticketDelete?: boolean;
+ userAdded?: boolean;
+ userRemoved?: boolean;
+ ticketRename?: boolean;
+ };
+ };
+ status?: {
+ enabled: boolean;
+ text?: string;
+ type?: "PLAYING" | "STREAMING" | "LISTENING" | "WATCHING" | "CUSTOM" | "COMPETING";
+ url?: string;
+ status: "online" | "idle" | "dnd" | "invisible";
+ };
+ 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,
+ {
+ name: string;
+ description?: string;
+ emoji?: string;
+ categoryId: string;
+ channelNameTemplate?: string;
+ message?: string;
+ welcomeContent?: string;
+ close?: {
+ channelMessage?: string;
+ dmMessage?: 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;
+ }>;
+ };
+ }
+ >;
+}
+
+interface ConfigVersions {
+ "0.0.1": ConfigV0_0_1;
+}
+
+type ConfigVersion = keyof ConfigVersions;
+
+type ConfigOf = ConfigVersions[V];
+
+export type VersionedConfig = {
+ version: V;
+} & ConfigOf;
+
+export type AnyVersionedConfig = VersionedConfig;
+
+export function defineConfig(version: V, config: ConfigOf): VersionedConfig {
+ return {
+ version,
+ ...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
new file mode 100644
index 00000000..b1eb76fe
--- /dev/null
+++ b/src/core/custom-id.ts
@@ -0,0 +1,55 @@
+/*
+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 {
+ 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))
+ };
+}
+
+/*
+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
new file mode 100644
index 00000000..54d4ce48
--- /dev/null
+++ b/src/core/defineCommand.ts
@@ -0,0 +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
new file mode 100644
index 00000000..5bdf9ef4
--- /dev/null
+++ b/src/core/defineEvent.ts
@@ -0,0 +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
new file mode 100644
index 00000000..650814ee
--- /dev/null
+++ b/src/core/defineFeature.ts
@@ -0,0 +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
new file mode 100644
index 00000000..36007ff3
--- /dev/null
+++ b/src/core/discovery.ts
@@ -0,0 +1,144 @@
+/*
+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";
+import type { Logger } from "@/core/logger";
+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 === "function" || (typeof value.data === "object" && value.data !== null)) &&
+ "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";
+}
+
+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 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,
+ (filePath) => isModuleFile(filePath) && !filePath.endsWith("index.ts") && !filePath.endsWith("index.js"),
+ isEventModule,
+ 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/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/logger.ts b/src/core/logger.ts
new file mode 100644
index 00000000..ac089e75
--- /dev/null
+++ b/src/core/logger.ts
@@ -0,0 +1,60 @@
+/*
+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;
+ error(message: string, ...meta: unknown[]): void;
+}
+
+function write(output: (...data: unknown[]) => void, level: string, message: string, meta: unknown[], scope: string) {
+ const prefix = `[${scope}] ${level}`;
+
+ if (meta.length === 0) {
+ output(prefix, message);
+ return;
+ }
+
+ output(prefix, message, ...meta);
+}
+
+export function createLogger(scope: string): Logger {
+ return {
+ info(message, ...meta) {
+ write(console.log, "\x1b[39;44mINFO\x1b[0m", message, meta, scope);
+ },
+ warn(message, ...meta) {
+ write(console.warn, "\x1b[39;43mWARN\x1b[0m", message, meta, scope);
+ },
+ error(message, ...meta) {
+ write(console.error, "\x1b[39;41mERROR\x1b[0m", message, meta, scope);
+ }
+ };
+}
+
+/*
+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
new file mode 100644
index 00000000..d6501d6c
--- /dev/null
+++ b/src/core/registry.ts
@@ -0,0 +1,105 @@
+/*
+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";
+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, LL }: CreateHandlerRegistryInput): HandlerRegistry {
+ const featureMap = new Map();
+ const commandMap = new Map();
+ const applicationCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = [];
+
+ 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 commands) {
+ 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(data.name, {
+ ...command,
+ data
+ });
+ applicationCommands.push(data);
+ }
+
+ logger.info(`Registered ${featureMap.size} feature modules.`);
+
+ return {
+ events,
+ features: featureMap,
+ commands: commandMap,
+ applicationCommands
+ };
+}
+
+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);
+ }
+}
+
+/*
+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
new file mode 100644
index 00000000..92a5d174
--- /dev/null
+++ b/src/core/respond.ts
@@ -0,0 +1,95 @@
+/*
+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,
+ APIMessageComponentInteraction,
+ APIModalSubmitInteraction,
+ CreateAutocompleteResponseOptions,
+ CreateInteractionDeferResponseOptions,
+ CreateInteractionFollowUpResponseOptions,
+ CreateInteractionResponseOptions,
+ CreateInteractionUpdateMessageResponseOptions,
+ CreateModalResponseOptions,
+ EditInteractionResponseOptions
+} from "@discordjs/core";
+import { MessageFlags } from "@discordjs/core";
+import type { BotApp } from "@/core/types";
+
+type ReplyableInteraction =
+ | APIChatInputApplicationCommandInteraction
+ | APIMessageComponentInteraction
+ | APIModalSubmitInteraction;
+
+type ModalCapableInteraction = APIChatInputApplicationCommandInteraction | APIMessageComponentInteraction;
+
+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?: CreateInteractionDeferResponseOptions) {
+ return app.client.api.interactions.defer(interaction.id, interaction.token, body);
+}
+
+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: CreateInteractionFollowUpResponseOptions) {
+ return app.client.api.interactions.followUp(app.applicationId, interaction.token, body);
+}
+
+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: CreateModalResponseOptions) {
+ return app.client.api.interactions.createModal(interaction.id, interaction.token, body);
+}
+
+export async function replyWithAutocomplete(
+ app: BotApp,
+ interaction: APIApplicationCommandAutocompleteInteraction,
+ body: CreateAutocompleteResponseOptions
+) {
+ return app.client.api.interactions.createAutocompleteResponse(interaction.id, interaction.token, body);
+}
+
+export async function replyWithError(app: BotApp, interaction: ReplyableInteraction) {
+ return reply(app, interaction, {
+ content: app.LL.shared.unexpected_interaction_error(),
+ 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
new file mode 100644
index 00000000..4256b29b
--- /dev/null
+++ b/src/core/router.ts
@@ -0,0 +1,195 @@
+/*
+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,
+ 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, CommandExecutionContext, ComponentExecutionContext, 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 command = this.app.registry.commands.get(interaction.data.name);
+
+ if (!command) {
+ this.app.logger.warn(`No slash command registered for "${interaction.data.name}".`);
+ return;
+ }
+
+ const context: CommandExecutionContext = {
+ app: this.app
+ };
+
+ await command.execute(context, interaction);
+ }
+
+ private async handleAutocomplete(interaction: APIApplicationCommandAutocompleteInteraction) {
+ const command = this.app.registry.commands.get(interaction.data.name);
+
+ if (!command?.autocomplete) {
+ return;
+ }
+
+ const context: CommandExecutionContext = {
+ app: this.app
+ };
+
+ await 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);
+ }
+}
+
+/*
+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
new file mode 100644
index 00000000..abc270d8
--- /dev/null
+++ b/src/core/types.ts
@@ -0,0 +1,118 @@
+/*
+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,
+ APIChatInputApplicationCommandInteraction,
+ APIMessageComponentInteraction,
+ APIModalSubmitInteraction,
+ Client
+} 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";
+
+export type RoutedInteraction =
+ | APIApplicationCommandAutocompleteInteraction
+ | APIApplicationCommandInteraction
+ | APIMessageComponentInteraction
+ | APIModalSubmitInteraction;
+
+export type CommandDataResolver =
+ | RESTPostAPIChatInputApplicationCommandsJSONBody
+ | ((LL: TranslationFunctions) => RESTPostAPIChatInputApplicationCommandsJSONBody);
+
+export interface CommandModule {
+ data: CommandDataResolver;
+ execute(context: CommandExecutionContext, interaction: APIChatInputApplicationCommandInteraction): Promise;
+ autocomplete?(context: CommandExecutionContext, interaction: APIApplicationCommandAutocompleteInteraction): Promise;
+}
+
+export interface FeatureModule {
+ key: string;
+ buttons?: Record;
+ stringSelects?: Record;
+ modals?: Record;
+}
+
+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;
+ config: AnyVersionedConfig;
+ applicationId: string;
+ logger: Logger;
+ locale: Locales;
+ LL: TranslationFunctions;
+ registry: HandlerRegistry;
+ router: InteractionRouterContract;
+}
+
+export interface FeatureContext {
+ app: BotApp;
+ feature: FeatureModule;
+}
+
+export interface CommandExecutionContext {
+ app: BotApp;
+}
+
+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;
+
+/*
+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
new file mode 100644
index 00000000..01d35523
--- /dev/null
+++ b/src/db/schema.ts
@@ -0,0 +1,74 @@
+/*
+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.
+
+export const panelMessagesTable = sqliteTable("panel_messages", {
+ panelKey: text().primaryKey(),
+ channelId: text().notNull(),
+ messageId: text().notNull(),
+ 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. */
+ 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(),
+ invitedUserIds: text().notNull().default("[]"),
+ /** UNIX time */
+ closedAt: int(),
+ closedBy: text(),
+ closedReason: text(),
+ transcriptUrl: text()
+});
+
+export type AppMetaRecord = typeof appMetaTable.$inferSelect;
+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
new file mode 100644
index 00000000..f5d87c90
--- /dev/null
+++ b/src/deploy-commands.ts
@@ -0,0 +1,82 @@
+/*
+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";
+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.js";
+
+config({ path: "./config/.env", quiet: true });
+const logger = createLogger("deploy");
+
+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.`);
+}
+
+export async function deployCommands() {
+ const [commands, events, features] = await Promise.all([
+ discoverCommands(logger),
+ discoverEvents(logger),
+ discoverFeatures(logger)
+ ]);
+ const i18n = createBotI18n(botConfig.lang, logger);
+ const registry = createHandlerRegistry({ commands, features, events, logger, LL: i18n.LL });
+
+ await deployApplicationCommands({
+ applicationCommands: registry.applicationCommands,
+ clientId: botConfig.clientId,
+ guildId: botConfig.guildId,
+ logger,
+ token: process.env.DISCORD_TOKEN
+ });
+}
+
+/*
+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/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
index 3a4d6b21..688a5d69 100644
--- a/src/events/interactionCreate.ts
+++ b/src/events/interactionCreate.ts
@@ -1,273 +1,46 @@
-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);
- }
+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.
- 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);
- }
- }
- }
- }
+Additional Term under GNU AGPL v3, Section 7(b):
- if (options.length <= 0) {
- interaction.editReply({
- content: this.client.locales.getValue("noTickets"),
- });
- return;
- }
+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.
- 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);
- }
+This notice must not be removed, obscured, or replaced.
+*/
- if (interaction.customId === "close_askReason") {
- closeAskReason(interaction, this.client, this.client.config.closeOption.deleteTicket);
- }
+import type { GatewayInteractionCreateDispatchData, ToEventProps } from "@discordjs/core";
+import { GatewayDispatchEvents, InteractionType } from "@discordjs/core";
+import { defineEvent } from "@/core/defineEvent";
- if (interaction.customId === "deleteTicket") {
- deleteTicket(interaction, this.client);
- }
+const interactionCreateEvent = defineEvent<[ToEventProps]>({
+ name: GatewayDispatchEvents.InteractionCreate,
+ async execute(app, event) {
+ if (event.data.type === InteractionType.Ping) {
+ return;
}
- 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 app.router.handleInteraction(event.data);
+ }
+});
- await interaction.update({
- content: `> Removed ${interaction.values.length < 1 ? interaction.values : interaction.values.map((a) => `<@${a}>`).join(", ")} from the ticket`,
- components: [],
- });
- }
- }
+export default interactionCreateEvent;
- 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);
- }
+/*
+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.
- 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);
- }
- }
- }
-}
+Additional Term under GNU AGPL v3, Section 7(b):
-/*
-Copyright 2023 Sayrix (github.com/Sayrix)
+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.
-Licensed under the Creative Commons Attribution 4.0 International
-please check https://creativecommons.org/licenses/by/4.0 for more informations.
+This notice must not be removed, obscured, or replaced.
*/
diff --git a/src/events/ready.ts b/src/events/ready.ts
index 86d529ff..f8eaa70a 100644
--- a/src/events/ready.ts
+++ b/src/events/ready.ts
@@ -1,307 +1,260 @@
-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)
+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.
-Licensed under the Creative Commons Attribution 4.0 International
-please check https://creativecommons.org/licenses/by/4.0 for more informations.
+This notice must not be removed, obscured, or replaced.
*/
-export default class ReadyEvent extends BaseEvent {
- private connected = false;
- constructor(client: ExtendedClient) {
- super(client);
- }
+import { GatewayDispatchEvents, type GatewayReadyDispatchData, type ToEventProps } from "@discordjs/core";
+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";
+import { announceTelemetryPrivacy, startTelemetry } from "@/telemetry";
- public async execute() {
- if (!this.client.config.guildId) {
- console.log("⚠️⚠️⚠️ Please add the guild id in the config.jsonc file. ⚠️⚠️⚠️");
- process.exit(0);
- }
+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,
+ 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;
- 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 readyEvent = defineEvent<[ToEventProps]>({
+ name: GatewayDispatchEvents.Ready,
+ 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}.`);
- 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);
+ await deployApplicationCommands({
+ applicationCommands: app.registry.applicationCommands,
+ clientId: app.config.clientId,
+ guildId: app.config.guildId,
+ logger: app.logger,
+ token: process.env.DISCORD_TOKEN
});
- 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);
- }
+ await syncTicketPanels(app);
+ await applyConfiguredPresence(app);
+ setInterval(() => {
+ 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);
+ }
+});
- 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);
+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);
- this.setStatus();
- setInterval(()=>this.setStatus(), 9e5); // 15 minutes
+ if ((permissions & PermissionFlagsBits.Administrator) !== PermissionFlagsBits.Administrator) {
+ warnStartup(app, "The bot does not have the Administrator permission. Some actions may fail because of missing permissions.");
+ }
- 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, "")
- );
+ for (const [panelKey, panel] of Object.entries(app.config.panels)) {
+ const channelId = panel.channelId.trim();
- 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 (!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 ((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, ""));
+ if (!PANEL_CHANNEL_TYPES.has(channel.type)) {
+ await failStartup(app, `Panel "${panelKey}" channel "${channelId}" is not a text-based channel.`);
}
+ }
+}
- this.connect(this.client.config.showWSLog);
+async function failStartup(app: BotApp, message: string, error?: unknown): Promise {
+ if (error) {
+ app.logger.error(message, error);
+ } else {
+ app.logger.error(message);
+ }
- this.client.deployCommands();
+ process.exit(1);
+}
+
+function warnStartup(app: BotApp, message: string, error?: unknown) {
+ if (error) {
+ app.logger.warn(message, error);
+ return;
}
- 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);
+ 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);
+
+ 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 [];
}
+}
- 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()}`);
- });
+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;
+ }
- 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);
+ 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);
+ }
+}
- ws.connect("wss://ws.ticket.pm", "echo-protocol");
+async function applyConfiguredPresence(app: BotApp) {
+ const configuredStatus = app.config.status;
+ if (!configuredStatus?.enabled) {
+ return;
}
- 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
- }
- })
- );
+ 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);
}
}
/*
-Copyright 2023 Sayrix (github.com/Sayrix)
+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.
-Licensed under the Creative Commons Attribution 4.0 International
-please check https://creativecommons.org/licenses/by/4.0 for more informations.
+This notice must not be removed, obscured, or replaced.
*/
diff --git a/src/features/commands/add/command.ts b/src/features/commands/add/command.ts
new file mode 100644
index 00000000..a2e3a0a6
--- /dev/null
+++ b/src/features/commands/add/command.ts
@@ -0,0 +1,122 @@
+/*
+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 { MessageFlags } from "@discordjs/core";
+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,
+ MAX_INVITED_TICKET_USERS,
+ updateInvitedUserIds
+} from "@/features/tickets/participants";
+import { getOpenTicketByChannel } from "@/features/tickets/records";
+import { getInteractionUser } from "@/features/tickets/utils";
+
+export default defineCommand({
+ data: (LL) => ({
+ name: "add",
+ description: LL.commands.add.description(),
+ options: [
+ {
+ name: "user",
+ 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: LL.commands.add.choose_user(),
+ 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: LL.commands.add.already_has_access(),
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ if (invitedUserIds.includes(selectedUser.userId)) {
+ await reply(app, interaction, {
+ content: LL.commands.add.already_invited(),
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ if (invitedUserIds.length >= MAX_INVITED_TICKET_USERS) {
+ await reply(app, interaction, {
+ content: LL.commands.add.invite_limit_reached({ limit: MAX_INVITED_TICKET_USERS }),
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ 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: LL.commands.add.success({ userId: selectedUser.userId }),
+ flags: MessageFlags.Ephemeral
+ });
+ }
+});
+
+/*
+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/commands/claim/command.ts b/src/features/commands/claim/command.ts
new file mode 100644
index 00000000..9f2ab52e
--- /dev/null
+++ b/src/features/commands/claim/command.ts
@@ -0,0 +1,40 @@
+/*
+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 { defineCommand } from "@/core/defineCommand";
+import { executeClaimCommand } from "@/features/tickets/claim-workflow";
+
+export default defineCommand({
+ data: (LL) => ({
+ name: "claim",
+ description: LL.commands.claim.description()
+ }),
+ execute: executeClaimCommand
+});
+
+/*
+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/commands/cleardm/command.ts b/src/features/commands/cleardm/command.ts
new file mode 100644
index 00000000..4dbda7b5
--- /dev/null
+++ b/src/features/commands/cleardm/command.ts
@@ -0,0 +1,91 @@
+/*
+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 { 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: (LL) => ({
+ name: "cleardm",
+ description: LL.commands.cleardm.description()
+ }),
+ async execute({ app }, interaction) {
+ const LL = app.LL;
+ await reply(app, interaction, {
+ content: LL.commands.cleardm.starting(),
+ 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: LL.commands.cleardm.dm_unavailable()
+ });
+ 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 ? LL.commands.cleardm.cleared({ count: deletedCount }) : LL.commands.cleardm.none_found()
+ });
+ }
+});
+
+/*
+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/commands/close/command.ts b/src/features/commands/close/command.ts
new file mode 100644
index 00000000..582f185d
--- /dev/null
+++ b/src/features/commands/close/command.ts
@@ -0,0 +1,40 @@
+/*
+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 { defineCommand } from "@/core/defineCommand";
+import { executeCloseCommand } from "@/features/tickets/close-workflow";
+
+export default defineCommand({
+ data: (LL) => ({
+ name: "close",
+ description: LL.commands.close.description()
+ }),
+ execute: executeCloseCommand
+});
+
+/*
+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/commands/mass_add/command.ts b/src/features/commands/mass_add/command.ts
new file mode 100644
index 00000000..a48a25c3
--- /dev/null
+++ b/src/features/commands/mass_add/command.ts
@@ -0,0 +1,189 @@
+/*
+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 { 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";
+import {
+ getInvitedUserIds,
+ grantTicketParticipantAccess,
+ MAX_INVITED_TICKET_USERS,
+ updateInvitedUserIds
+} from "@/features/tickets/participants";
+import { getOpenTicketByChannel } from "@/features/tickets/records";
+import { getInteractionUser } from "@/features/tickets/utils";
+
+export default defineCommand({
+ data: (LL) => ({
+ name: "mass_add",
+ description: LL.commands.mass_add.description(),
+ options: [
+ {
+ name: "users",
+ 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: LL.commands.mass_add.provide_users(),
+ 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);
+ 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, {
+ content: buildMassAddSummary(app, 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(
+ app: BotApp,
+ addedUserIds: string[],
+ skippedUserIds: string[],
+ invalidUserIds: string[],
+ limitReached: boolean
+) {
+ const LL = app.LL;
+ const lines: string[] = [];
+
+ if (addedUserIds.length > 0) {
+ lines.push(
+ LL.commands.mass_add.summary.added({
+ mentions: addedUserIds.map((userId) => `<@${userId}>`).join(", ")
+ })
+ );
+ } else {
+ lines.push(LL.commands.mass_add.summary.none_added());
+ }
+
+ if (skippedUserIds.length > 0) {
+ lines.push(LL.commands.mass_add.summary.skipped_existing({ count: skippedUserIds.length }));
+ }
+
+ if (invalidUserIds.length > 0) {
+ lines.push(LL.commands.mass_add.summary.skipped_invalid({ count: invalidUserIds.length }));
+ }
+
+ if (limitReached) {
+ lines.push(LL.commands.mass_add.summary.limit_reached({ limit: MAX_INVITED_TICKET_USERS }));
+ }
+
+ return lines.join("\n");
+}
+
+/*
+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/commands/remove/command.ts b/src/features/commands/remove/command.ts
new file mode 100644
index 00000000..40fcff8d
--- /dev/null
+++ b/src/features/commands/remove/command.ts
@@ -0,0 +1,222 @@
+/*
+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 { APIChatInputApplicationCommandInteraction, APIMessageComponentInteraction } from "@discordjs/core";
+import { ComponentType, MessageFlags } from "@discordjs/core";
+import { ApplicationCommandOptionType } from "discord-api-types/v10";
+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");
+
+export default defineCommand({
+ data: (LL) => ({
+ name: "remove",
+ description: LL.commands.remove.description(),
+ options: [
+ {
+ name: "user",
+ 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) {
+ 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: LL.commands.remove.no_invited_users(),
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ const selectedUser = getUserOption(interaction, "user");
+
+ if (selectedUser) {
+ await removeUsersFromTicket(app, interaction, openTicket.ticket, openTicket.ticketType.name, invitedUserIds, [
+ 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: LL.commands.remove.select_users(),
+ flags: MessageFlags.Ephemeral,
+ components: [
+ {
+ type: ComponentType.ActionRow,
+ components: [
+ {
+ type: ComponentType.StringSelect,
+ custom_id: REMOVE_USERS_CUSTOM_ID,
+ placeholder: LL.commands.remove.select_placeholder(),
+ 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,
+ 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[],
+ selectedUserIds: string[],
+ options?: {
+ responseMode?: "reply" | "update-message";
+ }
+) {
+ const removableUserIds = selectedUserIds.filter((userId) => invitedUserIds.includes(userId));
+
+ if (removableUserIds.length === 0) {
+ await respond(app, interaction, app.LL.commands.remove.not_invited(), options?.responseMode);
+ return;
+ }
+
+ for (const userId of removableUserIds) {
+ await revokeTicketParticipantAccess(app, ticket.channelId, userId);
+ }
+
+ await updateInvitedUserIds(
+ app,
+ 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,
+ interaction,
+ app.LL.commands.remove.success({
+ mentions: removableUserIds.map((userId) => `<@${userId}>`).join(", ")
+ }),
+ 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
+ });
+}
+
+/*
+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/commands/rename/command.ts b/src/features/commands/rename/command.ts
new file mode 100644
index 00000000..2adf7f3c
--- /dev/null
+++ b/src/features/commands/rename/command.ts
@@ -0,0 +1,117 @@
+/*
+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 { MessageFlags } from "@discordjs/core";
+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 { getInteractionUser, getMemberRoleIds, sanitizeChannelName } from "@/features/tickets/utils";
+
+export default defineCommand({
+ data: (LL) => ({
+ name: "rename",
+ description: LL.commands.rename.description(),
+ options: [
+ {
+ name: "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) {
+ await reply(app, interaction, {
+ content: openTicket.message,
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ if (!hasTicketStaffAccess(app, openTicket.ticketType, getMemberRoleIds(interaction))) {
+ await reply(app, interaction, {
+ content: LL.commands.rename.only_staff(),
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ const requestedName = getStringOption(interaction, "name");
+
+ if (!requestedName) {
+ await reply(app, interaction, {
+ content: LL.commands.rename.provide_name(),
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ 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: LL.commands.rename.success({ channelId: openTicket.ticket.channelId }),
+ flags: MessageFlags.Ephemeral
+ });
+ }
+});
+
+/*
+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/commands/shared/options.ts b/src/features/commands/shared/options.ts
new file mode 100644
index 00000000..53adc4b9
--- /dev/null
+++ b/src/features/commands/shared/options.ts
@@ -0,0 +1,59 @@
+/*
+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 { 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
+ };
+}
+
+/*
+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/commands/unclaim/command.ts b/src/features/commands/unclaim/command.ts
new file mode 100644
index 00000000..fdc25487
--- /dev/null
+++ b/src/features/commands/unclaim/command.ts
@@ -0,0 +1,40 @@
+/*
+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 { defineCommand } from "@/core/defineCommand";
+import { executeUnclaimCommand } from "@/features/tickets/claim-workflow";
+
+export default defineCommand({
+ data: (LL) => ({
+ name: "unclaim",
+ description: LL.commands.unclaim.description()
+ }),
+ execute: executeUnclaimCommand
+});
+
+/*
+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/service.ts b/src/features/logs/service.ts
new file mode 100644
index 00000000..d0a02ac7
--- /dev/null
+++ b/src/features/logs/service.ts
@@ -0,0 +1,189 @@
+/*
+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 { 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 = {
+ 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(app, LOG_TEMPLATE_REFERENCES[event.kind], createLogTokens(app, 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(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: formatClaimStatus(app, claimedById),
+ claimerId: claimedById ?? undefined,
+ claimerMention: claimedById ? `<@${claimedById}>` : undefined,
+ createdAt: ``,
+ createdById: event.ticket.createdById,
+ createdByMention: `<@${event.ticket.createdById}>`,
+ reason: getDefaultNoReason(app),
+ targetId: undefined,
+ targetMention: undefined,
+ 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: formatTranscriptStatus(app, null),
+ 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 = formatTranscriptStatus(app, event.transcriptUrl);
+ 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(app: BotApp, durationMs: number) {
+ const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
+
+ if (totalSeconds < 60) {
+ return `${totalSeconds}${app.LL.logs.duration.second_short()}`;
+ }
+
+ const units: Array<[label: string, seconds: number]> = [
+ [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;
+
+ 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/claim-workflow.ts b/src/features/tickets/claim-workflow.ts
new file mode 100644
index 00000000..a61aec89
--- /dev/null
+++ b/src/features/tickets/claim-workflow.ts
@@ -0,0 +1,280 @@
+/*
+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 { 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 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";
+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, app.LL.commands.claim.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, app.LL.commands.claim.already_claimed());
+ return;
+ }
+
+ if (ticket.claimedBy && !canTakeOverClaim(app, getMemberRoleIds(interaction))) {
+ await replyWithContent(app, interaction, app.LL.commands.claim.cannot_take_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);
+ void sendTicketLog(app, {
+ kind: "ticketClaim",
+ actor,
+ ticket: createTicketLogContext(nextTicketState, ticketType.name)
+ });
+
+ await replyWithContent(
+ app,
+ interaction,
+ 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, app.LL.commands.claim.disabled());
+ return;
+ }
+
+ if (!app.config.tickets.claims.allowUnclaim) {
+ await replyWithContent(app, interaction, app.LL.commands.unclaim.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) {
+ await replyWithContent(app, interaction, app.LL.commands.unclaim.not_claimed());
+ return;
+ }
+
+ if (ticket.claimedBy !== actor.id) {
+ await replyWithContent(app, interaction, app.LL.commands.unclaim.only_current_claimer());
+ 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
+ );
+ void sendTicketLog(app, {
+ kind: "ticketUnclaim",
+ actor,
+ ticket: createTicketLogContext(
+ {
+ ...ticket,
+ claimedBy: null
+ },
+ ticketType.name
+ )
+ });
+
+ await replyWithContent(app, interaction, app.LL.commands.unclaim.success());
+}
+
+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: app.LL.tickets.claim.only_staff()
+ };
+ }
+
+ 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: Pick,
+ ticketTypeName: string,
+ claimerUsername: string
+) {
+ await renameClaimedTicketChannel(app, ticket, ticketTypeName, claimerUsername);
+ await moveClaimedTicketChannel(app, ticket.channelId);
+}
+
+async function renameClaimedTicketChannel(
+ app: BotApp,
+ ticket: Pick,
+ 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
+ });
+}
+
+/*
+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/close-workflow.ts b/src/features/tickets/close-workflow.ts
new file mode 100644
index 00000000..68ff715e
--- /dev/null
+++ b/src/features/tickets/close-workflow.ts
@@ -0,0 +1,636 @@
+/*
+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,
+ APIChatInputApplicationCommandInteraction,
+ APIMessage,
+ APIMessageComponentInteraction,
+ APIModalSubmitInteraction
+} 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";
+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 {
+ appendMessageButton,
+ finalizeMessageTemplate,
+ hasMessageComponentCustomId,
+ loadMessageTemplate
+} 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 { 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";
+
+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: context.app.LL.tickets.records.not_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: context.app.LL.tickets.close.delete_channel_start(),
+ flags: MessageFlags.Ephemeral
+ });
+ void sendTicketLog(context.app, {
+ kind: "ticketDelete",
+ actor: getInteractionUser(interaction),
+ reason: manageable.ticket.closedReason ?? getDefaultNoReason(context.app),
+ transcriptUrl: manageable.ticket.transcriptUrl,
+ ticket: createTicketLogContext(manageable.ticket, manageable.ticketType.name)
+ });
+ 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: app.LL.tickets.close.modal.title(),
+ components: [
+ {
+ type: ComponentType.ActionRow,
+ components: [
+ {
+ type: ComponentType.TextInput,
+ custom_id: "reason",
+ label: app.LL.tickets.close.modal.reason_label(),
+ style: TextInputStyle.Paragraph,
+ required: false,
+ max_length: 500,
+ placeholder: app.LL.tickets.close.modal.reason_placeholder()
+ }
+ ]
+ }
+ ]
+ });
+ 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: app.LL.tickets.records.not_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
+ ? 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(app, 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) {
+ const invitedUserIds = getInvitedUserIds(ticket);
+
+ 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(app.LL.tickets.close.status.updating_access());
+ await removeClosedTicketParticipantAccess(app, ticket.channelId, ticket.createdBy, invitedUserIds);
+ }
+
+ const transcriptJob = app.config.tickets.close.createTranscript
+ ? await startTranscriptJob(app, ticket.channelId, {
+ 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) {
+ await status.update(app.LL.tickets.close.status.transcript_still_processing());
+ }
+
+ const closeMessageTokens = {
+ channelId: ticket.channelId,
+ claimStatus: formatClaimStatus(app, ticket.claimedBy),
+ claimerId: ticket.claimedBy ?? "",
+ claimerMention: ticket.claimedBy ? `<@${ticket.claimedBy}>` : "",
+ closerId: closer.id,
+ closerMention: `<@${closer.id}>`,
+ closerName: escapeDiscordMarkdown(closer.username),
+ reason: normalizedReason,
+ transcriptStatus: formatTranscriptStatus(app, transcriptUrl),
+ 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.deleteChannelOnClose && app.config.tickets.close.dmUserOnClose) {
+ await status.update(app.LL.tickets.close.status.sending_close_confirmation());
+ await sendCloseDm(app, ticket.createdBy, ticketType, closeMessageTokens);
+ }
+
+ 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 ? app.LL.tickets.close.deleted_with_transcript() : app.LL.tickets.close.deleted_without_transcript()
+ });
+ await app.client.api.channels.delete(ticket.channelId);
+ return;
+ }
+
+ 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
+ ? app.LL.tickets.close.status.sending_close_updates()
+ : app.LL.tickets.close.status.posting_close_summary()
+ );
+ await Promise.all(closeTasks);
+
+ await status.update(app.LL.tickets.close.status.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: app.LL.tickets.close.only_staff()
+ };
+ }
+
+ if (app.config.tickets.claims.enabled && app.config.tickets.claims.mode === "strict") {
+ if (!ticket.claimedBy) {
+ return {
+ ok: false as const,
+ message: app.LL.tickets.close.must_be_claimed()
+ };
+ }
+
+ if (ticket.claimedBy !== actorId) {
+ return {
+ ok: false as const,
+ message: app.LL.tickets.close.only_current_claimer()
+ };
+ }
+ }
+
+ 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: app.LL.tickets.records.not_ticket_channel()
+ };
+ }
+
+ const ticket = await findTicketByChannel(app, channelId);
+
+ if (!ticket) {
+ return {
+ ok: false as const,
+ message: app.LL.tickets.close.not_ticket()
+ };
+ }
+
+ if (!ticket.closedAt) {
+ return {
+ ok: false as const,
+ message: app.LL.tickets.close.only_closed_delete()
+ };
+ }
+
+ const ticketType = getTicketType(app, ticket.type);
+
+ if (!hasTicketStaffAccess(app, ticketType, roleIds)) {
+ return {
+ ok: false as const,
+ message: app.LL.tickets.close.only_staff_delete()
+ };
+ }
+
+ 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: 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) ? (disableTicketActionButton(component) as T) : component
+ ) as T[]
+ };
+}
+
+function disableTicketActionButton(button: APIButtonComponentWithCustomId): APIButtonComponentWithCustomId {
+ return {
+ ...button,
+ disabled: true
+ };
+}
+
+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();
+
+ if (!categoryId) {
+ return;
+ }
+
+ await app.client.api.channels.edit(channelId, {
+ parent_id: categoryId
+ });
+}
+
+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,
+ ticketType: ReturnType,
+ 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, resolveCloseDmMessageReference(app, ticketType), tokens);
+
+ await app.client.api.channels
+ .createMessage(dmChannel.id, {
+ ...finalizeMessageTemplate(messageTemplate)
+ })
+ .catch(() => undefined);
+}
+
+async function buildCloseChannelMessage(
+ app: BotApp,
+ ticketType: ReturnType,
+ 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, resolveCloseChannelMessageReference(app, ticketType), {
+ ...tokens,
+ deleteButtonCustomId
+ });
+
+ return finalizeMessageTemplate(
+ appendMessageButton(
+ messageTemplate,
+ !hasMessageComponentCustomId(messageTemplate, deleteButtonCustomId)
+ ? ({
+ type: ComponentType.Button,
+ custom_id: deleteButtonCustomId,
+ label: app.LL.tickets.actions.delete_ticket(),
+ style: ButtonStyle.Danger
+ } satisfies APIButtonComponentWithCustomId)
+ : undefined
+ )
+ );
+}
+
+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
+) {
+ let hasStarted = false;
+ 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;
+ displayedContent = content;
+ requestedContent = content;
+ await reply(app, interaction, {
+ content,
+ flags: MessageFlags.Ephemeral
+ });
+ },
+ update: async (content: string) => {
+ if (!hasStarted || content === requestedContent) {
+ return;
+ }
+
+ requestedContent = content;
+ await flushLatestContent();
+ }
+ };
+}
+
+function readCloseReason(interaction: APIModalSubmitInteraction) {
+ return getModalTextInputValues(interaction).get("reason")?.trim() || null;
+}
+
+function normalizeCloseReason(app: BotApp, reason: string | null) {
+ return reason?.trim() || getDefaultNoReason(app);
+}
+
+/*
+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/config-access.ts b/src/features/tickets/config-access.ts
new file mode 100644
index 00000000..f908e4d3
--- /dev/null
+++ b/src/features/tickets/config-access.ts
@@ -0,0 +1,90 @@
+/*
+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 { 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.`);
+ }
+}
+
+/*
+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/constants.ts b/src/features/tickets/constants.ts
new file mode 100644
index 00000000..46b7698e
--- /dev/null
+++ b/src/features/tickets/constants.ts
@@ -0,0 +1,41 @@
+/*
+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 { 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;
+
+/*
+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/feature.ts b/src/features/tickets/feature.ts
new file mode 100644
index 00000000..82344375
--- /dev/null
+++ b/src/features/tickets/feature.ts
@@ -0,0 +1,57 @@
+/*
+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 { 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;
+
+/*
+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
new file mode 100644
index 00000000..366f6842
--- /dev/null
+++ b/src/features/tickets/messages.ts
@@ -0,0 +1,494 @@
+/*
+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 { extname, resolve } from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+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,
+ LoadedMessageTemplate,
+ MessageTemplateComponent,
+ MessageTemplateSlotComponent,
+ MessageTemplateSource
+} 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): MessageTemplateSlotComponent {
+ return {
+ type: TEMPLATE_SLOT_TYPE,
+ slot,
+ slot_kind: TEMPLATE_SLOT_KIND_MANY
+ };
+}
+
+export function createPanelOpenerSlot() {
+ return createMessageSlot("panel-opener");
+}
+
+export function createRuntimeTextSlot() {
+ return createMessageSlot("runtime-text");
+}
+
+export async function loadMessageTemplate(
+ app: BotApp,
+ reference: string,
+ tokens?: Record
+): Promise {
+ const resolvedPath = await resolveMessageTemplatePath(reference);
+ const rawPayload = await loadMessageTemplateSource(resolvedPath);
+ 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);
+
+ return applyComponentsV2Defaults(renderedPayload);
+}
+
+export function finalizeMessageTemplate(payload: LoadedMessageTemplate): DiscordMessageTemplate {
+ return sanitizeMessageTemplate(applyComponentsV2Defaults(payload));
+}
+
+export function appendMessageText(
+ payload: LoadedMessageTemplate,
+ text: string | undefined,
+ options?: {
+ slot?: string;
+ }
+) {
+ 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
+ }
+ ],
+ options?.slot
+ );
+ }
+
+ 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
+ };
+ }
+
+ 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}.js`] : [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" || 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 JavaScript and 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): DiscordMessageTemplate {
+ const usesV2 = usesComponentsV2(payload);
+ const nextPayload: DiscordMessageTemplate = {};
+
+ 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: MessageTemplateComponent[] | undefined) {
+ return components?.some((component) => typeof component.type === "number" && COMPONENTS_V2_TYPES.has(component.type)) ?? false;
+}
+
+function injectManyIntoSlots(
+ components: MessageTemplateComponent[],
+ injectedComponents: APIMessageTopLevelComponent[],
+ slot: string
+): {
+ replaced: boolean;
+ value: MessageTemplateComponent[];
+} {
+ 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;
+ };
+
+ const value = visit(components) as MessageTemplateComponent[];
+
+ return {
+ replaced,
+ value
+ };
+}
+
+function appendComponentsToFirstContainer(
+ components: MessageTemplateComponent[],
+ appendedComponents: APIMessageTopLevelComponent[]
+): {
+ replaced: boolean;
+ value: MessageTemplateComponent[];
+} {
+ 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)]));
+ };
+
+ const value = visit(components) as MessageTemplateComponent[];
+
+ return {
+ replaced,
+ value
+ };
+}
+
+function stripTemplateSlots(components: MessageTemplateComponent[] | undefined): APIMessageTopLevelComponent[] | undefined {
+ if (!components?.length) {
+ return undefined;
+ }
+
+ 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 MessageTemplateSlotComponent {
+ 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;
+}
+
+/*
+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
new file mode 100644
index 00000000..d1df1018
--- /dev/null
+++ b/src/features/tickets/panel-sync.ts
@@ -0,0 +1,336 @@
+/*
+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,
+ APIMessageComponentInteraction,
+ APIMessageTopLevelComponent,
+ APIStringSelectComponent
+} from "@discordjs/core";
+import { ComponentType, MessageFlags } from "@discordjs/core";
+import { eq } from "drizzle-orm";
+import { createCustomId } from "@/core/custom-id";
+import { followUp, reply, updateMessage } 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, 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";
+
+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: context.app.LL.tickets.panel.no_visible_types(),
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ await reply(context.app, interaction, {
+ flags: MessageFlags.Ephemeral,
+ components: [createSelectRow(context.app, 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 updateMessage(context.app, interaction, {});
+ await followUp(context.app, interaction, {
+ content: context.app.LL.tickets.panel.select_type(),
+ flags: MessageFlags.Ephemeral
+ });
+ return;
+ }
+
+ const allowedTicketTypes = new Set(getPanelTicketTypeKeys(panel));
+
+ if (!allowedTicketTypes.has(ticketTypeKey)) {
+ await updateMessage(context.app, interaction, {});
+ await followUp(context.app, interaction, {
+ content: context.app.LL.tickets.panel.unavailable_type(),
+ 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(app, panel.message);
+ const withConfiguredText = appendMessageText(messageTemplate, panel.content);
+ const body = appendPanelOpener(withConfiguredText, buildPanelComponents(app, panelKey, panel));
+
+ return finalizeMessageTemplate({
+ ...body,
+ allowed_mentions: {
+ ...(body.allowed_mentions ?? {}),
+ parse: []
+ }
+ });
+}
+
+function buildPanelComponents(app: BotApp, panelKey: string, panel: PanelConfig): APIMessageTopLevelComponent[] {
+ switch (panel.opener.type) {
+ case "inline-select":
+ return [createSelectRow(app, 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(
+ app: BotApp,
+ 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 ?? app.LL.tickets.panel.select_placeholder(),
+ 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);
+}
+
+/*
+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
new file mode 100644
index 00000000..cd139b3b
--- /dev/null
+++ b/src/features/tickets/participants.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 { 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()
+ });
+}
+
+/*
+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
new file mode 100644
index 00000000..52b7b586
--- /dev/null
+++ b/src/features/tickets/records.ts
@@ -0,0 +1,74 @@
+/*
+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";
+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: app.LL.tickets.records.not_ticket_channel()
+ };
+ }
+
+ const ticket = await findTicketByChannel(app, channelId);
+
+ if (!ticket) {
+ return {
+ ok: false as const,
+ message: app.LL.tickets.records.not_open_ticket()
+ };
+ }
+
+ if (ticket.closedAt) {
+ return {
+ ok: false as const,
+ message: app.LL.tickets.records.already_closed()
+ };
+ }
+
+ return {
+ ok: true as const,
+ ticket,
+ 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
new file mode 100644
index 00000000..fa7b2ee0
--- /dev/null
+++ b/src/features/tickets/service.ts
@@ -0,0 +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/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
new file mode 100644
index 00000000..ce763d4a
--- /dev/null
+++ b/src/features/tickets/ticket-workflow.ts
@@ -0,0 +1,738 @@
+/*
+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,
+ APIAutoPopulatedSelectMenuComponent,
+ APIButtonComponentWithCustomId,
+ APIComponentInMessageActionRow,
+ APIMessageComponentInteraction,
+ APIMessageTopLevelComponent,
+ APIModalSubmitInteraction,
+ APISelectMenuComponent,
+ APIStringSelectComponent,
+ APITextInputComponent
+} from "@discordjs/core";
+import {
+ ButtonStyle,
+ ChannelType,
+ ComponentType,
+ MessageFlags,
+ OverwriteType,
+ 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";
+import type { BotApp, ComponentExecutionContext } from "@/core/types";
+import { type TicketRecord, ticketsTable } from "@/db/schema";
+import { sendTicketLog } from "@/features/logs/service";
+import {
+ getPanel,
+ getPanelTicketTypeKeys,
+ getTicketStaffRoleIds,
+ getTicketType,
+ userCanAccessTicketType
+} from "@/features/tickets/config-access";
+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,
+ TicketQuestionConfig,
+ TicketRenderTokens,
+ TicketTypeConfig
+} from "@/features/tickets/types";
+import {
+ getInteractionUser,
+ getMemberRoleIds,
+ getModalTextInputValues,
+ renderChannelName,
+ renderTemplate
+} from "@/features/tickets/utils";
+
+interface TicketOpenReasonData {
+ answers: string[];
+ combined: string;
+}
+
+type TicketOpenResponseMode = "initial" | "follow-up";
+type MessageActionRow = APIActionRowComponent;
+
+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 = getModalTextInputValues(interaction);
+ const reason =
+ questions.length > 0 ? createTicketOpenReason(context.app, questions, answers) : createDefaultTicketOpenReason(context.app);
+
+ 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);
+ const openForm = ticketType.openForm;
+ const hasOpenForm = Boolean(openForm?.questions.length);
+ const isStringSelect = interaction.data.component_type === ComponentType.StringSelect;
+
+ if (!userCanAccessTicketType(app, ticketType, roleIds)) {
+ if (isStringSelect) {
+ await updateMessage(app, interaction, {});
+ }
+
+ await respondToTicketOpen(app, interaction, app.LL.tickets.open.not_allowed_type(), isStringSelect ? "follow-up" : "initial");
+ return;
+ }
+
+ if (panel) {
+ const allowedTypes = new Set(getPanelTicketTypeKeys(panel));
+
+ if (!allowedTypes.has(context.ticketTypeKey)) {
+ 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) {
+ 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 (openForm?.questions.length) {
+ await showModal(app, interaction, {
+ custom_id: createCustomId("tickets", "submit-open-form", context.ticketTypeKey),
+ title: openForm.title,
+ components: openForm.questions.map((question) => ({
+ type: ComponentType.ActionRow,
+ components: [createQuestionInput(question)]
+ }))
+ });
+
+ if (isStringSelect) {
+ await refreshSourceMessageComponents(app, interaction);
+ }
+
+ return;
+ }
+
+ 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;
+ }
+
+ 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(
+ app: BotApp,
+ interaction: APIMessageComponentInteraction | APIModalSubmitInteraction,
+ ticketTypeKey: string,
+ ticketType: TicketTypeConfig,
+ reason: TicketOpenReasonData,
+ 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 createdAt = Date.now();
+ 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 = createTicketRenderTokens({
+ app,
+ channelId: channel.id,
+ createdByMention: `<@${user.id}>`,
+ openReason: 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 pinTicketWelcomeMessage(app, channel.id, ticketMessage.id);
+
+ await app.db.insert(ticketsTable).values({
+ channelId: channel.id,
+ creationMessageId: ticketMessage.id,
+ type: ticketTypeKey,
+ reason: serializeTicketOpenReason(reason),
+ createdBy: user.id,
+ 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: app.LL.tickets.open.created({ channelId: 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);
+}
+
+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,
+ 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 roleMentions = app.config.tickets.mentionRoleIds.map((roleId) => `<@&${roleId}>`);
+ const renderedTokens = {
+ ...tokens,
+ closeButtonCustomId,
+ staffMentions: roleMentions.length ? ` ${roleMentions.join(" ")}` : ""
+ };
+ 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" });
+ const buttons = buildTicketActionButtons(app, withRuntimeText, {
+ closeButtonCustomId,
+ claimButtonCustomId,
+ unclaimButtonCustomId,
+ claimedBy: tokens.claimerId,
+ disableActions: options?.disableActions ?? false
+ });
+ const body = attachWelcomeMessageActions(withRuntimeText, buttons);
+
+ 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 = createTicketRenderTokens({
+ app,
+ channelId: ticket.channelId,
+ claimStatus: formatClaimStatus(app, ticket.claimedBy),
+ claimerId: ticket.claimedBy ?? undefined,
+ claimerMention: ticket.claimedBy ? `<@${ticket.claimedBy}>` : undefined,
+ createdByMention: `<@${ticket.createdBy}>`,
+ openReason: parseStoredTicketOpenReason(app, 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);
+}
+
+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;
+ }
+): TicketActionRows | undefined {
+ const buttons: APIButtonComponentWithCustomId[] = [];
+
+ if (app.config.tickets.close.showCloseButton && !hasMessageComponentCustomId(payload, options.closeButtonCustomId)) {
+ buttons.push({
+ type: ComponentType.Button,
+ custom_id: options.closeButtonCustomId,
+ label: app.LL.tickets.actions.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: app.LL.tickets.actions.unclaim_ticket(),
+ style: ButtonStyle.Secondary,
+ disabled: options.disableActions
+ } satisfies APIButtonComponentWithCustomId)
+ : ({
+ type: ComponentType.Button,
+ custom_id: options.claimButtonCustomId,
+ label: app.LL.tickets.actions.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 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): APITextInputComponent {
+ 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 createDefaultTicketOpenReason(app: BotApp): TicketOpenReasonData {
+ return {
+ answers: [],
+ combined: getDefaultNoReason(app)
+ };
+}
+
+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(app, questions, normalizedAnswers)
+ };
+}
+
+function createTicketRenderTokens(input: {
+ app: BotApp;
+ 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.app, 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(app: BotApp, questions: TicketQuestionConfig[], answers: string[]): string {
+ const lines = questions.map((question, index) => {
+ const answer = answers[index] ?? getDefaultNoReason(app);
+
+ return app.LL.tickets.open.question_answer({
+ label: question.label,
+ answer
+ });
+ });
+ return lines.join("\n");
+}
+
+function normalizeAnswer(app: BotApp, answer: string | undefined) {
+ return answer?.trim() || getDefaultNoReason(app);
+}
+
+function serializeTicketOpenReason(reason: TicketOpenReasonData) {
+ if (!reason.answers.length) {
+ return reason.combined;
+ }
+
+ return JSON.stringify({
+ answers: reason.answers,
+ combined: reason.combined,
+ version: 1
+ });
+}
+
+function parseStoredTicketOpenReason(app: BotApp, reason: string | null | undefined): TicketOpenReasonData {
+ if (!reason) {
+ return createDefaultTicketOpenReason(app);
+ }
+
+ 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(app, typeof answer === "string" ? answer : undefined)),
+ combined: parsed.combined
+ };
+ } catch {
+ return {
+ answers: [],
+ combined: reason
+ };
+ }
+}
+
+/*
+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
new file mode 100644
index 00000000..6032715a
--- /dev/null
+++ b/src/features/tickets/transcripts.ts
@@ -0,0 +1,222 @@
+/*
+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";
+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;
+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,
+ 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, app.LL.tickets.transcript.collecting_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, app.LL.tickets.transcript.creating());
+
+ 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, app.LL.tickets.transcript.uploading());
+
+ const result = await uploadClient.uploadDraftTranscript(draftTranscript, {
+ uuidStyleIds: app.config.uuidType !== "emoji",
+ 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}`;
+}
+
+async function fetchAllMessages(app: BotApp, channelId: string) {
+ const messages: TranscriptSourceMessage[] = [];
+ let before: string | undefined;
+
+ while (true) {
+ const batch = await app.client.api.channels.getMessages(channelId, {
+ limit: 100,
+ before
+ });
+
+ if (batch.length === 0) {
+ break;
+ }
+
+ // 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;
+ }
+
+ 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(app: BotApp, 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(
+ app.LL.tickets.transcript.progress({
+ 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);
+ })
+ ]);
+}
+
+/*
+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
new file mode 100644
index 00000000..ee75ada9
--- /dev/null
+++ b/src/features/tickets/types.ts
@@ -0,0 +1,101 @@
+/*
+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 { APIComponentInContainer, APIContainerComponent } from "discord-api-types/v10";
+import type { Locales, TranslationFunctions } from "../../../i18n/i18n-types.js";
+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 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?: MessageTemplateComponent[];
+ flags?: number;
+ useComponentsV2?: boolean;
+}
+
+export type DiscordMessageTemplate = Omit & {
+ components?: APIMessageTopLevelComponent[];
+};
+
+export interface MessageTemplateContext {
+ locale: Locales;
+ LL: TranslationFunctions;
+}
+
+export type MessageTemplateSource = LoadedMessageTemplate | ((context: MessageTemplateContext) => LoadedMessageTemplate);
+
+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;
+}
+
+/*
+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
new file mode 100644
index 00000000..f72901d7
--- /dev/null
+++ b/src/features/tickets/utils.ts
@@ -0,0 +1,141 @@
+/*
+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 { 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) {
+ 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 escapeDiscordMarkdown(value: string) {
+ return value.replaceAll(/([\\_*`~|>])/g, "\\$1");
+}
+
+export function sanitizeChannelName(value: string) {
+ const cleaned = value
+ .trim()
+ .toLowerCase()
+ .replaceAll(/\s+/g, "-")
+ // Deduplicate dashes
+ .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 : [];
+}
+
+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.
+
+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 f6bfde65..088f30ad 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,79 +1,129 @@
/*
-Copyright 2023 Sayrix (github.com/Sayrix)
+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.
-Licensed under the Creative Commons Attribution 4.0 International
-please check https://creativecommons.org/licenses/by/4.0 for more informations.
-*/
+Additional Term under GNU AGPL v3, Section 7(b):
-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";
+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.
-// Initalize .env file as environment
-try {envconf();}
-catch {console.log(".env failed to load");}
+This notice must not be removed, obscured, or replaced.
+*/
-// 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);
-});
+import { config } from "dotenv";
+import { createBotApp } from "@/app";
+import { createLogger } from "@/core/logger";
+import { BOT_VERSION } from "@/version";
-process.on("uncaughtException", (err: string) => {
- console.error(err);
-});
+const REPOSITORY_TAGS_URL = "https://api.github.com/repos/Sayrix/Ticket-Bot/tags";
+const VERSION_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)$/;
+const logger = createLogger("boot");
-process.stdout.write(`
+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
- 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");
+config({ path: "./config/.env", quiet: true });
+void checkForUpdates();
+
+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) => {
+ logger.error("Failed to start bot", error);
+ process.exit(1);
});
-const config: ConfigType = jsonc.parse(fs.readFileSync(path.join(__dirname, "/../config/config.jsonc"), "utf8"));
+async function checkForUpdates() {
+ try {
+ const response = await fetch(REPOSITORY_TAGS_URL, {
+ headers: {
+ accept: "application/vnd.github+json"
+ }
+ });
+
+ 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);
-const client = new ExtendedClient({
- intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers],
- presence: {
- status: config.status?.status ?? "online"
+ 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;
}
-}, 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);
+ 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;
+}
/*
-Copyright 2023 Sayrix (github.com/Sayrix)
+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.
-Licensed under the Creative Commons Attribution 4.0 International
-please check https://creativecommons.org/licenses/by/4.0 for more informations.
+This notice must not be removed, obscured, or replaced.
*/
diff --git a/src/migrate-v3-config.ts b/src/migrate-v3-config.ts
new file mode 100644
index 00000000..94233e66
--- /dev/null
+++ b/src/migrate-v3-config.ts
@@ -0,0 +1,559 @@
+/*
+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..b5e82ead
--- /dev/null
+++ b/src/migrate-v3-db.ts
@@ -0,0 +1,659 @@
+/*
+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.
+*/
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/telemetry.ts b/src/telemetry.ts
new file mode 100644
index 00000000..a8801782
--- /dev/null
+++ b/src/telemetry.ts
@@ -0,0 +1,280 @@
+/*
+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 = 90_000; // 1 minute and 30 seconds.
+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() {
+ const bunVersion = (process.versions as Record).bun;
+
+ if (typeof bunVersion === "string") {
+ return {
+ name: "bun",
+ version: bunVersion
+ } 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.
+*/
diff --git a/src/types/config.d.ts b/src/types/config.d.ts
new file mode 100644
index 00000000..fe974ef5
--- /dev/null
+++ b/src/types/config.d.ts
@@ -0,0 +1,29 @@
+/*
+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
new file mode 100644
index 00000000..fa005bea
--- /dev/null
+++ b/src/types/discordjs-rest.d.ts
@@ -0,0 +1,37 @@
+/*
+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 });
+ public setToken(token: string | undefined): 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
new file mode 100644
index 00000000..affa9428
--- /dev/null
+++ b/src/types/index.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.
+*/
+
+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
new file mode 100644
index 00000000..4af5de51
--- /dev/null
+++ b/src/types/process.d.ts
@@ -0,0 +1,41 @@
+/*
+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 {
+ namespace NodeJS {
+ interface ProcessEnv extends Record {
+ DB_FILE_NAME: string;
+ DISCORD_TOKEN: string;
+ TICKETPM_PASSKEY?: 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/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/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.
+*/
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 25b5b7ee..bb3d00b4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,20 +1,34 @@
{
- "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", "DOM", "DOM.Iterable"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "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": ["node"],
+
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "exclude": ["dist", "node_modules"]
+}